深入研究 Runloop 与线程保活
深入研究 Runloop 与线程保活
在讨论 runloop 相关的文章,以及分析 AFNetworking(2.x) 源码的文章中,我们经常会看到关于利用 runloop 进行线程保活的分析,但如果不求甚解的话,极有可能因此学会了一个错误的用法,本文就来分析一下其中常见的误区。
我提供了一个 Demo,可以在我的 Github 上下载并运行一遍,文章中只提供了部分代码。
AFN 中的实现
首先我们知道在旧版本的AFN 中使用了 NSURLConnection 来发起并处理网络连接。AFN 的做法是把网络请求的发起和解析都放在同一个子线程中进行,但由于子线程默认不开启 runloop,它会向一个 C语言程序那样在运行完所有代码后退出线程。而网络请求是异步的,这会导致获取到请求数据时,线程已经退出,代理方法没有机会执行。因此,AFN 的做法是使用一个 runloop 来保证线程不死,也就是下面这段被讲烂了的代码:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
当然,单独看这一个方法意义不大,我们稍微结合一下上下文,看看这个方法在哪里被调用:
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_networkRequestThread start];
}); return _networkRequestThread;
}
似乎这种写法提供了一种思路:“如果需要在子线程中异步执行操作,可以利用 runloop 进行线程保活”。但准确的来说,AFN 的这种写法并不能实现我们的需求,它只是在 AFN 这个特殊场景下可以工作。
不信你可以尝试阅读一下第二段代码,看看它和平时使用 NSThread 时有什么区别,如果没看出来也无妨,先记住这段代码,我们稍后分析。
NSThread 与内存泄漏
这种写法的第一个问题就是存在内存泄漏。我们构造以下用例,其实就是把 AFN 的线程创建放在一个循环里:
- (void)memoryTest {
for (int i = ; i < ; ++i) {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
}
- (void)run {
@autoreleasepool {
NSLog(@"current thread = %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
if (!self.emptyPort) {
self.emptyPort = [NSMachPort port];
}
[runLoop addPort:self.emptyPort forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
奇怪的事情出现了,尽管是在 ARC 环境下,内存依然不停的上涨。如果我们把 run 方法中和 runloop 相关的代码删除则不会出现上述问题,显然,开启 runloop 导致了内存泄漏,也就是 thread 对象无法释放。
这里的 emptyPort 用来维持 runloop 的运行,根据官方文档的描述,如果 runloop 中没有任何 modeItem,就不会启动,而是立刻退出。之所以选择作为属性而不是临时变量,是因为我发现每次调用 [NSMachPort port] 方法都会占用内存,原因暂时不清楚。
我们可以尝试手动结束 runloop 并关闭线程:
- (void)memoryTest {
for (int i = ; i < ; ++i) {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
[self performSelector:@selector(stopThread) onThread:thread withObject:nil waitUntilDone:YES];
}
}
- (void)stopThread {
CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];
}
很遗憾,这依然没有任何效果。而且不难猜测是我们没有能正确的结束 runloop 的运行。
Runloop 的启动与退出
考验英文水平的时候到了,首先来看一段官方文档对于如何启动 runloop 的介绍,它的启动方式一共有三种:
- Unconditionally
- With a set time limit
- In a particular mode
这三种进入方式分别对应了三种方法,其中第一种就是我们目前使用的:
- run
- runUntilDate
- runMode:beforeDate:
接下来分别是对三种方式的介绍,文字比较啰嗦,这里我简单总结一下,有兴趣的读者可以直接看原文。
- 无条件进入是最简单的做法,但也最不推荐。这会使线程进入死循环,从而不利于控制 runloop,结束 runloop 的唯一方式是 kill 它。
- 如果我们设置了超时时间,那么 runloop 会在处理完事件或超时后结束,此时我们可以选择重新开启 runloop。这种方式要优于前一种
- 这是相对来说最优秀的方式,相比于第二种启动方式,我们可以指定 runloop 以哪种模式运行。
查看 run 方法的文档还可以知道,它的本质就是无限调用 runMode:beforeDate: 方法,同样地,runUntilDate: 也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。
总结来说,runMode:beforeDate: 表示的是 runloop 的单次调用,另外两者则是循环调用。
相比于 runloop 的启动,它的退出就比较简单了,只有两种方法:
- 设置超时时间
- 手动结束
如果你使用方法二或三来启动 runloop,那么在启动的时候就可以设置超时时间。然而考虑到目标是:“利用 runloop 进行线程保活”,所以我们希望对线程和它的 runloop 有最精确的控制,比如在完成任务后立刻结束,而不是依赖于超时机制。
好在根据文档的描述,我们还可以使用 CFRunLoopStop() 方法来手动结束一个 runloop。注意文档中在介绍利用 CFRunLoopStop() 手动退出时有下面这句话:
The difference is that you can use this technique on run loops you started unconditionally.
这里的解释非常容易产生误会,如果在阅读时没有注意到 exit 和 terminate 的微小差异就很容易掉进坑里,因为在 run 方法的文档中还有这句话:
If you want the run loop to terminate, you shouldn't use this method
总的来说,如果你还想从 runloop 里面退出来,就不能用 run 方法。根据实践结果和文档,另外两种启动方法也无法手动退出。
正确的做法
难道子线程中开启了 runloop 就无法结束并释放了么?这显然是一个不合理的结论,经过一番查找,终于在这篇文章里找到了答案,它给出了使用 CFRunLoopStop() 无效的原因:
CFRunLoopStop() 方法只会结束当前的 runMode:beforeDate: 调用,而不会结束后续的调用。
这也就是为什么 Runloop 的文档中说 CFRunLoopStop() 可以 exit(退出) 一个 runloop,而在 run 等方法的文档中又说这样会导致 runloop 无法 terminate(终结)。
文章中给出的方案是使用 CFRunLoopRun() 启动 runloop,这样就可以通过 CFRunLoopStop() 方法结束。而文档则推荐了另一种方法:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
我尝试了文档提供的方法,确实不会导致内存泄漏,但不方便验证 runloop 是否真的开启,然后又被终止。所以我实际采用的是第一种方案:
- (void)memoryTest {
for (int i = ; i < ; ++i) {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
[self performSelector:@selector(stopThread) onThread:thread withObject:nil waitUntilDone:YES];
}
}
- (void)stopThread {
CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];
}
- (void)run {
@autoreleasepool {
NSLog(@"current thread = %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
if (!self.emptyPort) {
self.emptyPort = [NSMachPort port];
}
[runLoop addPort:self.emptyPort forMode:NSDefaultRunLoopMode];
[runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];
}
}
验证
采用上述方案后,确实可以观察到不会再出现内存泄漏问题,但这并不是终点。因为我们还需要验证 runloop 确实在启动后被关闭。
为了证明 runloop 确实启动,我设计了如下方法:
- (void)printSomething {
NSLog(@"current thread = %@", [NSThread currentThread]);
[self performSelector:@selector(printSomething) withObject:nil afterDelay:];
}
我们知道 performSelector:withObject:afterDelay 依赖于线程的 runloop,因为它本质上是由一个定时器负责定期加入到 runloop 中执行。所以如果这个方法可以成功执行,说明当前线程的 runloop 已经开启,否则则说明没有启动。
为了证明 runloop 可以被终止,我创建了一个按钮,在点击按钮时执行以下方法:
- (void)stopButtonDidClicked:(id)sender {
[self performSelector:@selector(stopRunloop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)stopRunloop {
CFRunLoopStop(CFRunLoopGetCurrent());
}
成功的观察到点击按钮后,控制台不再有日志输出,因此证明 runloop 确实已经停止。
总结
啰嗦了这么多,其实是为了研究如何利用 runloop 实现线程保活。要注意的地方主要有以下点:
- 了解 runloop 实现线程保活的原理,注意添加的那个空 port
- 了解 runloop 导致的线程对象内存泄漏问题
- 了解 runloop 的几种启动方式以及彼此之间的关联
- 了解 runloop 的释放方式和原理
由于相关资料的匮乏以及个人水平有限,虽然竭力研究但仍不保证绝对的正确性,欢迎交流指正。
最后,文章开头对 AFN 的分析留作一个简单的思考题,为什么 AFN 中的用法不会有问题?
深入研究 Runloop 与线程保活的更多相关文章
- iOS开发——高级篇——线程保活
线程保活: 顾名思义,就是保护线程不死(保证线程处于激活状态,生命周期没有结束) 正常情况,当线程执行完一次任务之后,需要进行资源回收,也就意味着生命周期结束 应用场景: 当有一个任务,随时都有可能去 ...
- IOS RunLoop 常驻线程的实现
线程常驻,正如其名,我们要实现的事让一个线程长期存在,不被销毁. 这时会有人说,那还不简单吗. 但是这里我们要实现的事如何让线程座椅待命,而且并不是主线程. 首先介绍一下正常情况下的线程使用. // ...
- 使用runloop阻塞线程的正确写法
使用runloop阻塞线程的正确写法 runloop可以阻塞线程,等待其他线程执行后再执行. 比如: @implementation ViewController{ BOOL end;}…– ( ...
- runloop和线程有什么关系?
每条线程都有唯一的一个RunLoop对象与之对应的 主线程的RunLoop是自动创建并启动 子线程的RunLoop需要手动启动 子线程的RunLoop创建步骤如下: 获得RunLoop对象后要调用ru ...
- RunLoop与线程,RunLoop的作用--runloop模型即位事件处理模型
runloop的三大责任:生命周期管理.通信.调度. 一.线程缺省生命周期基础上的生命周期管理: 二.线程接收外部信息的通道 通过消息队列实现. 三.RunLoop即是消息处理循环,也是事件调度策略机 ...
- c#中多线程同步Lock(锁)的研究以及跨线程UI的操作
本文只针对C#中,多线程同步所用到的锁(lock)作为研究对象.由于想更直观的显示结果,所以,在做demo的时候,就把多线程通过事件操作UI的代码也写了出来,留作备忘和分享吧. 其实多线程的同步,使用 ...
- c#中Lock(锁)的研究以及跨线程UI的操作
本文只针对C#中,多线程同步所用到的锁(lock)作为研究对象.由于想更直观的显示结果,所以,在做demo的时候,就把多线程通过事件操作UI的代码也写了出来,留作备忘和分享吧. 其实多线程 ...
- c#中多线程同步Lock(锁)的研究以及跨线程UI的操作 (转)
https://www.cnblogs.com/tommyheng/p/4104552.html 本文只针对C#中,多线程同步所用到的锁(lock)作为研究对象.由于想更直观的显示结果,所以,在做de ...
- runloop与线程的关系
随机推荐
- 在VS Code中对Python进行单元测试
在VS Code中对Python进行单元测试 Python扩展支持使用Python的内置unittest框架以及pytest和Nose进行单元测试.要使用pytest和Nose,必须将它们安装到当前的 ...
- C#并行编程(1):理解并行
什么是并行 并行是指两个或者多个事件在同一时刻发生. 在程序运行中,并行指多个CPU核心同时执行不同的任务:对于单核心CPU,严格来说是没有程序并行的.并行是为了提高任务执行效率,更快的获取结果. 与 ...
- 学机器学习,不会数据处理怎么行?—— 一、NumPy详解
最近学习强化学习和机器学习,意识到数据分析的重要性,就开始补Python的几个科学计算库,并总结到博客中.本篇博客中用到的代码在这里下载. 什么是Numpy? NumPy是Python数值计算最重要的 ...
- 潭州课堂25班:Ph201805201 并发(非阻塞,epoll) 第十课 (课堂笔记)
# -*- coding: utf-8 -*- # 斌彬电脑 # @Time : 2018/7/12 0012 20:29 import socket server = socket.socket() ...
- BZOJ4221 : JOI2012 kangaroo
将袋鼠大小和口袋大小分别从小到大排序. 枚举从左往右第一只没有被放入任何口袋的袋鼠$x$,那么$x$之前的所有袋鼠.以及$x$能装入的所有口袋都应该在匹配边上. 按这只袋鼠将上下两个序列分为两部分,设 ...
- 【11.8校内测试】【倒计时2天】【状压DP】【随机化?/暴力小模拟】
Solution 数据范围疯狂暗示状压,可是一开始发现状态特别难受. 将每一层的奇偶性状压,预处理所有状态的奇偶性.每一层的输入代表的其实可以是下一层某个点可以被从这一层哪些点转移到. 所以枚举每个状 ...
- Maven入门指南⑥:将项目发布到私服
1 . 修改私服中仓库的部署策略 Release版本的项目应该发布到Releases仓库中,对应的,Snapshot版本应该发布到Snapshots仓库中.Maven根据pom.xml文件中版本号&l ...
- CentOS 7下简单的Ansible使用入门
1.配置hosts文件,Ansible依赖hosts文件进行主机通讯,不能直接在命令行上直接输入IP. vi /etc/ansible/hosts hosts文件格式如下: [servers] hos ...
- 嵌入式Linux学习(一)
嵌入式系统 嵌入式系统是一种专用的计算机系统,作为装置或设备的一部分.通常,嵌入式系统是一个控制程序存储在ROM中的嵌入式处理器控制板.事实上,所有带有数字接口的设备,如手表.微波炉.录像机.汽车等, ...
- AngularJS中页面呈现html代码段
如何在页面呈现一段html代码段呢? 在textarea中我们这样写: <textarea name="" id="" cols="30&quo ...