Run loop 类似 Windows 中的消息循环,作用是管理线程,让线程可以接收、处理消息,在没有工作时休眠。
下图是 run loop 模型:
在图中可以看到:Run loop 是一个死循环,在每个循环开始时会检查是否达到设定的退出时间,然后依次检查监听的各种事件源(input source/performSelector/timer)是否有新事件产生,如果有就执行相应的回调。当一次循环结束时就会让线程休眠,直到被 Timer 或者其他事件源唤醒。
线程与 run loop 是一一对应的,一个 run Loop 包含了多个 run loop mode,每个 mode 又包含了多个事件源。它们之间的关系如下图。
下面来逐一介绍各个部分。
Run Loop Mode
一个 run loop mode 包含了一组 input sources、一组 timer 和一组 run loop 的 observer。Run loop 总是运行在某个 run loop mode 下,这个时候只有这个 run loop mode 中的事件才会被接收和处理,observer 才会被通知。如果一个 input sources 产生的事件并没有被监听就会被 hold 住,直到 run loop 运行在合适的 mode 下。
在代码中,可以通过名称来指定需要的 mode。然后至少添加一个或者多个 input sources/timer/observer。在大多数情况下,我们只需要将 run loop 运行在默认的 mode 下即可,但是在特定情况下(比如某些对实时性要求比较高的场景)可以通过创建一个自定义的 mode 来过滤一些不需要处理的事件。
常用的预定义的 mode 有以下几种:
- NSDefaultRunLoopMode:默认的 mode,通常情况下都使用它。
- NSEventTrackingRunLoopMode:涉及到用户交互(如鼠标拖动、或者 UIScrollView 滚动)时使用的 mode。
- NSRunLoopCommonModes:Run loop 有个 _commonModeItems 属性,其中保存了一些 input sources/timer/observer,这些 item 是所有的 Common Modes 所共享的。自定义的 mode 也可以将自身标记为 Common Mode。
一个例子:
Github 上一个开源的查看图片的项目 MWPhotoBrowser 在显示照片列表时有一个 bug:在用户滚动 UICollectionView
的时候,新显示的图片加载不出来。
原因是:MWPhotoBrowser 内部加载完图片之后会用 performSelector:withObject:afterDelay:
调用 postCompleteNotification
方法抛出一个 MWPHOTO_LOADING_DID_END_NOTIFICATION
消息。这个时候默认使用的是 default mode,因此在 UICollectionView
滚动停止之前,主线程都不会接收到这个消息。解决的方法也很简单,将抛消息这个动作添加到 Common Modes 即可。
[self performSelector:@selector(postCompleteNotification) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
Input Sources
Input Sources 分为两类:自定义(source0)和基于 mach port(source1)和。Mac port 是进程间通信的一种方法。两种 source 产生的事件处理方法是一致的,唯一的不同是基于 mach port 的事件是自动触发的,而自定义的事件需要手动触发。
例如调用 performSelector:onThread:
时,系统会添加一个 input source 到指定的线程上,并且在执行完之后将自身从相应的 run loop 中移除。此时对应的线程必须要有一个运行着的 run loop,否则 selector 将不会被执行(还记得主线程的 run loop 会由系统来运行,而我们自己创建的线程需要自己显式运行)。
一个例子:
苹果会帮 app 注册一个基于 mach port 的事件源,并设置了一个回调函数 _UIApplicationHandleEventQueue()
。当用户每一次点击屏幕上的一个按钮时,系统会通过 mach port 将事件发送给 app,app 通过回调来相应事件。
Timer Source
Timer 和其他的 source 一样,是需要添加到某个 mode 中才能被监听。如果在 timer 的触发时间到了,而 run loop 没有在其所在的 mode 下运行时,对应的事件就不会被触发,直到前面的条件满足。
如果 timer 的触发时间到了,而 run loop 正在处理其他的事件,那么 timer 的事件就会在 run loop 的下一个循环中处理。因此,timer 并不一定会在指定的时间准时触发。
Observers
在 run loop 执行的各个阶段,系统还会发送相应的通知,通过注册监听这些消息来做一些额外的工作。可以监听的消息有:
- Run loop 的入口。
- Run loop 即将处理一个 timer。
- Run loop 即将处理一个 input source。
- Run loop 即将休眠。
- Run loop 被唤醒(还没有处理任何事件)。
- Run loop 即将退出。
一个例子:
在 ARC 环境下,一个 autorelease 对象会在一次 run loop 循环周期的结束时释放。苹果是这样实现的:
- 监听 run loop 的入口消息,调用
_wrapRunLoopWithAutoreleasePoolHandler()
创建新的内存池。 - 监听 run loop 即将休眠的消息,调用
_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的内存池并创建新内存池。 - 监听 run loop 退出消息,调用
_objc_autoreleasePoolPop()
来释放内存池。
运行和停止 Run Loop
在运行后台线程的 run loop 之前,至少需要添加一个 input source 或者 timer,否则 run loop 会在运行后立刻退出。在添加一个 timer 后,每当 timer 触发时线程都会被唤醒,而添加 input source 时,只在对应的事件发生时才会唤醒线程,因此相比较于添加 timer,添加 input source 是一个更高效的方法。
一个例子:
在 AFNetworking 中创建一个常驻线程的方法是让 run loop 监听一个 port based input source:
+ (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;
}
这里添加的 input source 并没有实际作用,只是为了让线程不退出而已。在真正需要处理任务的时候使用 performSelector:onThread:
来把任务丢到这个线程。
值得注意的是:NSRunLoop 类不是线程安全的,因此必须只在一个线程内操作自己对应的 run loop。
要停止一个 run loop 有两种方法:在运行 run loop 时指定一个超时时间或者显式调用 CFRunLoopStop
。
Run Loop 的应用场景
系统默认会创建并运行一个主线程的 run loop。对于开发者自己创建的线程,用户需要显式地运行这个 run loop。之所以要这样设计是因为是某些情况下 run loop 是不需要运行的:比如你需要执行一个定义好的长期运行的任务。Run loop 适用于交互性比较强的任务:
- 需要使用 mach port 或者其他方式与其他线程通信。
- 需要使用 timer。
- 需要使用
performSelector
相关的方法。 - 需要保证线程不退出来执行一些周期性的任务。