在讨论 runloop 相关的文章,以及分析 AFNetworking(2.x) 源码的文章中,咱们常常会看到关于利用 runloop 进行线程保活的分析,但若是不求甚解的话,极有可能所以学会了一个错误的用法,本文就来分析一下其中常见的误区。html
我提供了一个 Demo,能够在个人 Github 上下载并运行一遍,文章中只提供了部分代码。ios
首先咱们知道在旧版本的AFN 中使用了 NSURLConnection 来发起并处理网络链接。AFN 的作法是把网络请求的发起和解析都放在同一个子线程中进行,但因为子线程默认不开启 runloop,它会向一个 C语言程序那样在运行完全部代码后退出线程。而网络请求是异步的,这会致使获取到请求数据时,线程已经退出,代理方法没有机会执行。所以,AFN 的作法是使用一个 runloop 来保证线程不死,也就是下面这段被讲烂了的代码:git
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}复制代码
固然,单独看这一个方法意义不大,咱们稍微结合一下上下文,看看这个方法在哪里被调用:github
+ (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 这个特殊场景下能够工做。swift
不信你能够尝试阅读一下第二段代码,看看它和平时使用 NSThread
时有什么区别,若是没看出来也无妨,先记住这段代码,咱们稍后分析。网络
这种写法的第一个问题就是存在内存泄漏。咱们构造如下用例,其实就是把 AFN 的线程建立放在一个循环里:app
- (void)memoryTest {
for (int i = 0; i < 100000; ++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] 方法都会占用内存,缘由暂时不清楚。oop
咱们能够尝试手动结束 runloop 并关闭线程:ui
- (void)memoryTest {
for (int i = 0; i < 100000; ++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 的介绍,它的启动方式一共有三种:
这三种进入方式分别对应了三种方法,其中第一种就是咱们目前使用的:
接下来分别是对三种方式的介绍,文字比较啰嗦,这里我简单总结一下,有兴趣的读者能够直接看原文。
查看 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 = 0; i < 100000; ++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:1];
}复制代码
咱们知道 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 实现线程保活。要注意的地方主要有如下点:
因为相关资料的匮乏以及我的水平有限,虽然竭力研究但仍不保证绝对的正确性,欢迎交流指正。
最后,文章开头对 AFN 的分析留做一个简单的思考题,为何 AFN 中的用法不会有问题?