初识 Run Loop

Run loop 类似 Windows 中的消息循环,作用是管理线程,让线程可以接收、处理消息,在没有工作时休眠。

下图是 run loop 模型:

runloop.jpg

图片来源

在图中可以看到:Run loop 是一个死循环,在每个循环开始时会检查是否达到设定的退出时间,然后依次检查监听的各种事件源(input source/performSelector/timer)是否有新事件产生,如果有就执行相应的回调。当一次循环结束时就会让线程休眠,直到被 Timer 或者其他事件源唤醒。

线程与 run loop 是一一对应的,一个 run Loop 包含了多个 run loop mode,每个 mode 又包含了多个事件源。它们之间的关系如下图。

Run Loop

字符画工具 asciiflow

下面来逐一介绍各个部分。

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 相关的方法。
  • 需要保证线程不退出来执行一些周期性的任务。

Ref

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容