Unit 4

Runloop与线程有什么关系?

Runloop的概念

一般来讲,线程只能线性地执行任务,执行完成后线程就会退出。如果我们需要某种机制来构成一个闭环,让线程能随时处理事件但并不退出,通常的代码逻辑就会是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称为Event Loop,在macOSiOS中就是Runloop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

Runloop实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

它们之间的关系

在iOS中,每个线程(包括主线程)都有一个Runloop对象与之对应,在主线程中Runloop是默认启动的,因为所有关于UI的更新必须放在主线程完成,它是在主函数中被创建启动的:

int main(int argc, char * argv[]) {
   @autoreleasepool {
       return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
   }
}

这就解释了为什么应用程序能够随时响应用户的交互(故障状态除外)。对于其他线程来说,Runloop默认是没有启动的。因为通常多线程任务是确定的,不需要用户交互的,当然你也可以手动配置和启动来实现线程更加复杂的功能。

Runloop准备休眠之前,会释放旧的自动释放池并创建新的。当然在Runloop即将退出时,就会释放所有的自动释放池而不会在创建新的。

以上林林总总,Runloop与线程的关系可归结为一句话:Runloop是主线程任务执行的基石,是其他线程扩展功能必要条件。

以+ scheduledTimerWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

原因

Runloop只能在一种模式(Mode)下运行,模式主要用来指定事件在运行循环中的优先级,共有如下几种:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态;
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合;
  • UITrackingRunLoopMode:ScrollView滑动时;
  • UIInitializationRunLoopMode:启动时。

只有前两种模式是公开提供的,如果要更改Runloop的模式,必须退出当前的Runloop,更改模式后重新启动。基于这样的一个机制,为了保证UIScrollView的滑动顺畅,在UIScrollView实例滚动时(滚动结束后,自动回置原态),Runloop将更改模式为UITrackingRunLoopMode。而用+ scheduledTimerWithTimeInterval:target:selector: userInfo:repeats:方法触发的定时器将被添加到NSDefaultRunLoopMode模式下的Runloop,不同模式下的Runloop分别管理着自己的若干个Source/Timer/Observer,这便导致了定时器的暂停回调。

解决办法

换一种方式初始化定时器并指定加入Runloop的模式,如下:

_timer = [NSTimer timerWithTimeInterval:3.0f target:self selector:@selector(changeViewBackgroundColor:) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

以下是Apple官方文档对这个模式的说明:

This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially.

这是一个可配置的常用模式组。将输入源与此模式联系起来也可以将其与该组中的每个模式联系起来。对于Cocoa applications,默认情况下,此集合包括默认、模态和事件跟踪模式。Core Foundation仅仅包含了默认模式的初始化。

Apple是如何实现autoreleasepool?

autoreleasepool是以一个栈的形式实现的,主要通过以下三个函数完成:

  • objc_autoreleasepoolPush
  • objc_autoreleasepoolPop
  • objc_autorelease

从函数名不难看出对应的逻辑:当一个对象加入到自动释放池时,调用objc_autoreleasepoolPush()压入;当一个对象要被销毁时,调用objc_autorelease释放对象并调用objc_autoreleasepoolPop()将该对象弹出自动释放池。

在block内如何修改block外部变量?为什么如此就可修改?

问题的解决是很简单的,即在外部变量声明前加__block(双下划线)关键字修饰。然而当你遇到这个问题时,你要小心,因为面试官想要的答案不可能这么浅显,你需要给出理由。

block从本质上可以归入函数范畴,其内部的代码是通过函数指针来指向的。block有三种类型:NSGlobalBlockNSStackBlockNSMallocBlock,更详尽的信息可以在这篇文章里找到。对于有访问外部变量且能通过block名就能调用的block属于第二类(勘误:第三类),代码段存储在堆内存中。而局部变量(__block只能修饰局部变量,但block内部除了修改外部的局部变量,还可以修改类的成员变量和属性,因为它们是存放在堆中的。)的存储空间是在栈里的。所以说当局部变量进入到block中时,就涉及到了作用域变换问题。以下是一系列测试代码:

//以这种方式获取地址,最终结果是字符串在堆内存常量区中的地址。无疑,它们是相同的。
__block NSString *str = @"str";
NSLog(@"%p", str);

void (^test)(void) = ^() {
    NSLog(@"%p", str);
    str = @"";
};

test();

//将获取地址换成下面的形式,此时获取的就是真正意义上局部变量str的地址
NSLog(@"%p", &str);

//打印结果依次是:
// 0x7fff5022b8f8  栈区地址
// 0x608000055238  堆区地址

//对于类来说,对象的内存是分配在堆中的,它从类获得的属性和成员变量自然也是如此,所以在block中可以读取或修改

//...

@property (strong, nonatomic) NSNumber *age;

//..

_age = @0;

NSLog(@"%p", _age);

void (^test)(void) = ^() {
    NSLog(@"%p", _age);
    _age = 1;
};

//打印结果:
//0xb000000000000002
//0xb000000000000002

//换成如下语句获取地址,结果依然相同
NSLog(@"%p", &_age);

//打印结果:
//0x7f805dd07168
//0x7f805dd07168

现在就可以总结__block关键字产生了什么效果:__block修饰的局部变量进入到block内部时,系统会将局部变量复制到堆区,且以后对它的访问都是对堆区的这个局部变量进行操作。

从两者的内存地址也可以得出相同的结论:iOS的一个进程栈区内存只有1M,Mac也只有8M,而前后的内存地址偏移量远远大于这个值,故block内部访问的被__block修饰的局部变量是位于堆区的。

其实,在堆区内读取外部局部变量是读取该变量所指向堆内存中实际存放的值,而上面说论述的这种block自然可以合法读取此变量的值,因为它们位于同一作用域。而想要修改该变量的值,就必须通过该局部变量在栈区的内存地址以指针指向的方式来修改,__block修饰就是为了让堆区内有这样的一个指针指向从而合法修改变量值。这就好比你引用别人的论文需要贴出原文的出处,而你想要修改别人的论文你就必须通过和原作者协商的方式来达成目的,最后由原作者发表声明并修改该论文,但如果你有原作者修改论文的授权(姑且认为这样可行)就另当别论了。

GCD的队列(dispatch_queue_t)分哪两种类型?

  • 串行队列: Serial Dispatch Queue
  • 发队列: Concurrent Dispatch Queue

参考并整理自:

@微博@iOS程序犭袁:iOSInterviewQuestions
sunnyxx:招聘一个靠谱的iOS
CocoaChina:深入理解Runloop

内容概要:本文详细介绍了一个基于Java和Vue的联邦学习隐私保护推荐系统的设计实现。系统采用联邦学习架构,使用户数据在本地完成模型训练,仅上传加密后的模型参数或梯度,通过中心服务器进行联邦平均聚合,从而实现数据隐私保护协同建模的双重目标。项目涵盖完整的系统架构设计,包括本地模型训练、中心参数聚合、安全通信、前后端解耦、推荐算法插件化等模块,并结合差分隐私同态加密等技术强化安全性。同时,系统通过Vue前端实现用户行为采集个性化推荐展示,Java后端支撑高并发服务日志处理,形成“本地训练—参数上传—全局聚合—模型下发—个性化微调”的完整闭环。文中还提供了关键模块的代码示例,如特征提取、模型聚合、加密上传等,增强了项目的可实施性工程参考价值。 适合人群:具备一定Java和Vue开发基础,熟悉Spring Boot、RESTful API、分布式系统或机器学习相关技术,从事推荐系统、隐私计算或全栈开发方向的研发人员。 使用场景及目标:①学习联邦学习在推荐系统中的工程落地方法;②掌握隐私保护机制(如加密传输、差分隐私)模型聚合技术的集成;③构建高安全、可扩展的分布式推荐系统原型;④实现前后端协同的个性化推荐闭环系统。 阅读建议:建议结合代码示例深入理解联邦学习流程,重点关注本地训练全局聚合的协同逻辑,同时可基于项目架构进行算法替换功能扩展,适用于科研验证工业级系统原型开发。
单元测试是一种软件开发中的测试方法,主要用于验证软件中最小可测试单元(通常是函数或方法)的正确性。在单元测试中,开发者会为每一个函数或方法编写测试用例,以确保其行为符合预期。以下是关于 Unit4 单元测试的一些使用方法和教程: ### Unit4 单元测试的使用方法 Unit4 通常指的是某个特定的测试框架或工具,但根据上下文来看,这里可能是指单元测试相关的第四单元内容,或者测试工具(如 Visual Unit 4)相关。以下是一些通用的单元测试使用方法: 1. **测试框架选择**: - 在进行单元测试时,选择合适的测试框架非常重要。例如,在 C/C++ 开发中,Visual Unit 是一个常用的单元测试工具,它支持对代码进行补齐、隔离、控制等操作,从而帮助开发者高效地完成测试工作[^3]。 2. **测试用例设计**: - 在设计测试用例时,需要考虑各种边界条件和异常情况。例如,对于一个函数,不仅要测试其正常输入的情况,还要测试其在极端输入(如最大值、最小值)和非法输入(如空指针、无效参数)下的行为。 3. **测试执行调试**: - 单元测试的执行通常通过自动化测试框架完成。例如,Visual Unit 提供了图形化界面和命令行工具,可以方便地运行测试用例并生成测试报告。如果测试失败,可以通过调试工具快速定位问题所在。 4. **测试覆盖率分析**: - 测试覆盖率是衡量单元测试质量的一个重要指标。通过分析测试覆盖率,可以了解哪些代码路径已经被测试覆盖,哪些尚未覆盖。Visual Unit 提供了白盒覆盖分析功能,帮助开发者确保测试的全面性[^2]。 5. **持续集成自动化测试**: - 在现代软件开发中,单元测试通常持续集成(CI)系统结合使用。每次代码提交后,CI 系统会自动运行所有单元测试,确保新代码不会破坏现有功能。 ### Unit4 单元测试的实践建议 - **边开发边测试**:在编写代码的同时编写单元测试用例,有助于及时发现并修复问题,提高开发效率。 - **代码简洁性**:保持代码逻辑的简洁性,有助于减少测试的复杂度。过于复杂的架构可能导致测试用例难以编写和维护。 - **重构优化**:在开发过程中,可能会进行代码重构。重构后应重新运行所有单元测试,确保重构未引入新的问题。 - **模块化设计**:虽然模块化设计可能会增加类和函数的数量,但合理的模块划分有助于提高代码的可测试性和可维护性。 ### 示例:使用 Visual Unit 4 进行单元测试 假设你正在使用 Visual Unit 4 进行 C/C++ 项目的单元测试,以下是一个简单的测试用例示例: ```cpp #include "vunit.h" // 测试函数:加法运算 int add(int a, int b) { return a + b; } // 测试用例 TEST_CASE("Test add function") { // 测试正常输入 CHECK(add(2, 3) == 5); // 测试负数输入 CHECK(add(-1, -1) == -2); // 测试边界值 CHECK(add(INT_MAX, 0) == INT_MAX); } ``` 在上述示例中,`TEST_CASE` 宏定义了一个测试用例,`CHECK` 宏用于断言测试结果是否符合预期。通过 Visual Unit 4 的测试框架,可以轻松运行这些测试用例并查看测试结果。 ### 单元测试的挑战思考 - **测试架构的平衡**:在实际开发中,测试的全面性和代码架构的复杂性之间可能存在矛盾。例如,过多的类和函数嵌套可能导致测试用例难以编写和维护。因此,开发者需要在代码逻辑的简洁性和模块化设计之间找到平衡点[^4]。 - **性能可读性**:有时为了简化测试,可能会牺牲一定的性能或架构的美观性。然而,这种权衡需要根据项目的具体需求来决定。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值