MJ 说:就算有 1-2 年的 iOS 经验都觉得这个东西(RunLoop)一头雾水。
又有人说:你就算玩烂了 RunLoop 也未必找到工作。

个人学习记录:RunLoop 初识

大致了解 RunLoop

RunLoop,从字面上看就是指 运行循环、跑圈 的意思,主要的作用是用来:

  • 保持 App 的持续运行
  • 处理 App 中的各种事件(比如触摸事件、定时器事件、Selector 事件)
  • 节省 CPU 资源,提高程序性能:就是该做事的时候做事,该休息的时候休息

简单来说,RunLoop 是 App 一直存在并不断处理事件的原因。

我们知道,iOS App 的入口就是 main 函数,即:

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

在一开始的时候,我纳闷这个函数什么时候会把返回值返回,但是同时也纳闷,如果这个函数真的把返回值返回了的话,那这个 App 不就挂掉了吗?后来才发现这一句代码

1
UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))

内部就启动了一个 RunLoop,因此 UIApplicationMain 函数一直没有返回(可以大致理解为在里面有一个死循环),保持了 App 的持续运行,而且,这个默认启动的 RunLoop 是跟主线程(UI 线程)相关联的,主要处理跟主线程(UI 线程)相关的事情,比如说在主线程添加的点击事件、在主线程添加的定时器事件、在主线程的 UI 刷新事件等,这些事件都是在这个默认启动的 RunLoop 里做的。

附:验证这个 main 函数没有把返回值返回

1
2
3
4
5
6
7
int main(int argc, char * argv[]) {
@autoreleasepool {
int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"-->");
return result;
}
}

这个 Log 不会打印。

我们可以大致可以这样认为,如果没有 RunLoop:

如果没有 RunLoop

那么程序在第 3 行之后就结束了。

如果有了 RunLoop:

如果有了 RunLoop

由于 main 函数里面启动了一个 RunLoop,所以程序并不会马上退出,保持持续运行的状态。

NSRunLoop 对象

大概了解了 RunLoop 原来是那么 把炮 的之后,肯定是要想办法用到我们自己的 App 上。

在 iOS 开发中,有两套 API 供我们去访问和使用 RunLoop

  • Foundation 框架(NSRunLoop)
  • Core Foundation 框架(CFRunLoopRef)

NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop 对象,它们是等价的,可以相互转换,只不过 NSRunLoop 是 Apple 基于 CFRunLoopRef 做的一层 OC 封装,不开源。所以说,如果要了解 RunLoop 内部的结构,只能去研究 CFRunLoopRef(开源的)

附:Apple 官方文档:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

CFRunLoopRef 开源代码:http://opensource.apple.com/source/CF/CF-1151.16/

在上面说到,在 App 启动的时候,UIApplicationMain 函数内部就启动了一个 RunLoop,用来处理与主线程相关的事情,那么线程和 RunLoop 之间有什么恩怨情仇呢?

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象,一条线程最多有一个 RunLoop(不单单是主线程、子线程也是)。
  • 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要手动创建并开启。
  • RunLoop 的生命周期:在第一次获取的时候创建,在线程结束的时候销毁。

注意:

  • RunLoop 对象是利用字典来进行存储的,而且 key 是对应的线程,value 是对应的 RunLoop。
  • 在子线程中创建 RunLoop,不是通过 alloc init 来创建的,而是直接通过 [NSRunLoop currentRunLoop] 来创建,由于它本身是懒加载的,所以如果不存在的话就会自动创建一个与该线程相对应的 RunLoop 对象返回。
  • 在子线程中,如果不是手动创建 RunLoop 的话,那么子线程内部是不会创建 RunLoop 的(可以下载 CFRunloopRef 的源码,搜索 _CFRunloopGet0,查看代码)。

RunLoop 相关方法:

1
2
3
4
5
6
7
// Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; // 获得主线程的 RunLoop 对象
// Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
CFRunLoopGetMain(); // 获得主线程的 RunLoop 对象

由于 NSRunLoop 是闭源的,因此想要研究 RunLoop,就研究 Core Foundation 框架的 RunLoop,以下是相关的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CFRunLoopMode:RunLoop 的运行模式,注意:CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装
typedef CFStringRef CFRunLoopMode CF_EXTENSIBLE_STRING_ENUM;
// CFRunLoopRef:和 Foundation 的 NSRunLoop 对应
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
// CFRunLoopSourceRef:RunLoop 要处理的事件源(输入源)
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoopObserverRef:RunLoop 的观察者(监听者)
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoopTimerRef:基于时间的触发器
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

小 Tip:在 Core Foundation 中,凡是后面加了 Ref 的,都是指针。

这些类之间的恩怨情仇如下:

RunLoop 相关类的恩怨情仇

RunLoop 想要运行起来(进行循环),就得指定一个 CFRunLoopMode(简称 Mode),而且这个 Mode 里面得至少有 Source(Source0、Source1)、Timer 其中一个,如果都没有,那么这个 RunLoop 就是空的 RunLoop,就不会执行循环,直接退出循环。

注意:如果仅有 Observer 的话,那么这个 RunLoop 还是空的 RunLoop,就不会执行循环,直接退出循环。查看源码即可发现:

1
2
3
4
5
6
// 在 static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode); 方法可以发现有以下判断:
// 判断 Source 和 Timer 是否为空,并没有判断 Observer
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;

RunLoop 的原理图:

Structure of a run loop and its sources

想要理解 RunLoop ,得先理解这些类。

CFRunLoopMode

CFRunLoopMode(Run Loop Modes,以下简称 Mode) 代表 RunLoop 的运行模式。

  • 一个 RunLoop 可以包含若干个 Mode,每个 Mode 又包含若干个 Source / Observer / Timer
  • 每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称为 currentMode
  • 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入
  • 这样做的目的是为了分隔开不同组的 Source / Observer / Timer,让其互不影响
  • 切换到一个 Mode 的时候,就只会处理这个 Mode 下的 Source / Observer / Timer,专心做这个 Mode 下的一些操作

系统默认注册了 5 个 Mode:

  • kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 UIScrollView 追踪触摸滚动,保证界面滚动时不受其他 Mode 影响(在这种 Mode 下 UIScrollView 就会专注于滚动的操作,让滚动更加流畅)。
  • kCFRunLoopCommonModes:这是一个占位用的 Mode,不是一种真正的 Mode。
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到。

PS:以上的 Mode 和 https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html 有点不同,后面具体看看。

这让我想起了项目的经历,就是做自动无限轮播 Banner 的时候,以及做一元夺宝的订单列表倒计时(UITableView 的每个 cell 都有一个倒计时)的时候,都要用到定时器 NSTimer,代码是这样的:

1
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(startAutoScroll) userInfo:nil repeats:YES]; // ①

但是这样做有一个缺点就是:当我滚动 UITableView 的时候,cell 上的倒计时会暂停工作,后来再加上这句话就可以了:

1
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; // ②

代码 ① 创建的 NSTimer 是默认添加到 kCFRunLoopDefaultMode 的,所以 NSTimer 只能在 kCFRunLoopDefaultMode 运行。所以在 UITableView 不滚动的时候,是能够运行的,当 UITableView 滚动的时候,RunLoop 切换到 UITrackingRunLoopMode,NSTimer 就不能运行了。

加上了代码 ②,RunLoop 把 NSTimer 添加到 NSRunLoopCommonModes 下,NSRunLoopCommonModes 是占位的 Mode,不是真正的 Mode,可以理解为一个标签,通过打印我们可以发现,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 都是打了 NSRunLoopCommonModes 标签的,因此 NSTimer 又能够运行在 kCFRunLoopDefaultMode,也能够运行在 NSRunLoopCommonModes。因此当 UITableView 不滚动时,RunLoop 是 kCFRunLoopDefaultMode,NSTimer 能够运行;当 UITableView 滚动时,RunLoop 是 UITrackingRunLoopMode,NSTimer 也能够运行。

注:以下两段代码的效果一样:

1
2
3
4
5
6
// 第一段,这个 timer 时默认添加到 RunLoop 的,而且是 NSDefaultRunLoopMode 的,因此能运行
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(startAutoScroll) userInfo:nil repeats:YES];
// 第二段,这个 timer 时没有添加到 RunLoop 的,要手动添加到 RunLoop 才能运行
self.timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(startAutoScroll) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,它基本上说的就是 NSTimer,受 RunLoop 的 Mode 影响(上面已经有提到)。

但是 GCD 的定时器不受 RunLoop 的 Mode 影响(因为 GCD 不在 Mode 里面,所以不受它控制)。

因为 RunLoop 要处理的东西比较多:Source、Timer、Observer,所以有时候用 NSTimer 实现倒计时并不是很准确,这个时候可以使用 GCD 实现定时器。

GCD 实现定时器

我们知道,dispatch_after 这个函数可以实现延时执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"^^^");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2.0s 后我终于执行了");
});
NSLog(@"&&&");
}
// 运行结果:
^^^
&&&
2.0s 后我终于执行了

但是定时器就是另外的函数了,Xcode 已支持提示:

Timer

我一敲回车,震惊了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"^^^");
// 获得队列并创建一个定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0));
// 设置定时器的各种属性(什么时候开始,每隔多长时间执行一遍)
// GCD 的时间参数:一般来说是纳秒(1s = 10^9ns)
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
// 设置定时器的回调
dispatch_source_set_event_handler(timer, ^{
// code to be executed when timer fires
NSLog(@"code to be executed when timer fires");
});
// 启动定时器 fire~
dispatch_resume(timer);
NSLog(@"&&&");
}
// 运行结果:
^^^
&&&

科科,说好的定时器呢,怎么都没有进入回调?原因是 timer 是局部变量,会被释放掉,可以配置成为属性就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@interface ViewController ()
@property (strong, nonatomic) dispatch_source_t timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"^^^");
// 获得队列并创建一个定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0));
// 设置定时器的各种属性(什么时候开始,每隔多长时间执行一遍)
// GCD 的时间参数:一般来说是纳秒(1s = 10^9ns)
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
// 设置定时器的回调
dispatch_source_set_event_handler(self.timer, ^{
// code to be executed when timer fires
NSLog(@"code to be executed when timer fires");
});
// 启动定时器 fire~
dispatch_resume(self.timer);
NSLog(@"&&&");
}
@end
小 Tip:
// 如果要设置定时器在 4.0s 后开始,可以把这个传入
// dispatch_time(DISPATCH_TIME_NOW, 4.0 * NSEC_PER_SEC);
// 即:
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, 4.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, when, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);

嗯,终于可以了,不过看起来有点麻烦。

CFRunLoopSourceRef

CFRunLoopSourceRef 是 RunLoop 的事件源(输入源,Input Sources),比如常见的触摸事件、点击事件等,在 Apple 官方文档我们可以看到按照概念来分的话,有 3 类:

  • Port-Based Sources:基于端口的源,和其他线程相交互的,通过内核来发消息的,都是用这个 Port-Based Sources。

    从 Apple 官方文档大致可以知道,Cocoa 和 Core Foundation 都支持通过使用端口相关的对象和方法来创建基于端口的输入源,例如:在 Cocoa 中你完全不需要自己创建输入源,只需要简简单单创建一个端口对象并且使用 NSPort 的方法把端口添加到 RunLoop 就可以了。但是在 Core Foundation 中你就必须手动创建端口和 RunLoop 输入源,blablabla(大致如此,最好看 Apple 官方文档)

  • Custom Input Sources:自定义输入源,暂时开发中比较少用。

    自个看 Apple 官方文档。

  • Cocoa Perform Selector Sources:基于 Perform Selector 源。

    比如以下的这些:

Cocoa Perform Selector Sources

如果按照函数调用栈来分的话,可以分为以下 2 种:

  • Source0:非基于 Port(端口)的
  • Source1:基于 Port(端口)的,通过内核和其他线程通信,接收、分发系统事件,比如触摸事件、点击事件

我们写一个按钮的点击事件,在回调事件里打了个断点,可以看到上面所说的函数调用栈。

按钮点击事件

这些暂时来说,大概了解、知道这么一回事就 OK 了。

CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者(监听器),能够监听 RunLoop 的状态改变。

可以监听的时间点有以下几个(在 CFRunLoop.h 可以找到):

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1,即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2,即将处理 Timer 事件
kCFRunLoopBeforeSources = (1UL << 2), // 4,即将处理 Source 事件
kCFRunLoopBeforeWaiting = (1UL << 5), // 32,即将进入休眠状态
kCFRunLoopAfterWaiting = (1UL << 6), // 64,刚从休眠中被唤醒
kCFRunLoopExit = (1UL << 7), // 128,即将退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // A combination of all the preceding stages.
};

一个小知识点:1UL << 0 是逐位左移的意思,其实就是按位取值。UL 是无符号长整数的意思,可以不必理会。用二进制表示如下:

1 << 0 表示二进制的 00000001,即十进制 1;

1 << 2 表示二进制的 00000100,即十进制 4;

如果想要监听 RunLoop 的状态改变,只能使用 Core Foundation 的 CFRunLoopRef,Foundation 的 NSRunLoop 是没有提供相应的方法的,NSRunLoop 有一个 addObserver 方法,不过是 KVO 的,而不是用来监听 RunLoop 的状态改变的。

小 Tips:每一次进入 kCFRunLoopEntry 状态,就代表进入了新的 RunLoop。就好比点击手势可以监听到有几种手势状态:开始点击、手松开等状态,从开始点击到结束就代表一次手势。RunLoop 也是一样,从 kCFRunLoopEntry 到 kCFRunLoopBeforeWaiting 也算是一次 RunLoop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad {
[super viewDidLoad];
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 回调处理
NSLog(@"activity -> %zd", activity);
});
// 添加观察者,监听 RunLoop 的某个 Mode 下的状态改变
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// Core Foundation 的内存管理(不受 ARC 影响)
// 凡是带有 Create、Copy、Retain 等字眼的函数创建出来的对象,都需要在最后做一次 Release,如 CFRunLoopObserverCreate...
// Release 函数:CFRelease(要释放的对象);
CFRelease(observer);
}

运行后,发现打印很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2016-11-02 14:43:58.067 Moke[4321:848015] 即将进入 Loop
2016-11-02 14:43:58.067 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.068 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.257 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.257 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.265 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.265 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.265 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.265 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.265 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.266 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.266 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.266 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.266 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.266 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.267 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.267 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.267 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.267 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.268 Moke[4321:848015] 即将进入到休眠状态
2016-11-02 14:43:58.378 Moke[4321:848015] 刚从休眠中被唤醒
2016-11-02 14:43:58.381 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:58.381 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:58.381 Moke[4321:848015] 即将进入到休眠状态
2016-11-02 14:43:59.212 Moke[4321:848015] 刚从休眠中被唤醒
2016-11-02 14:43:59.213 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:43:59.213 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:43:59.213 Moke[4321:848015] 即将进入到休眠状态
2016-11-02 14:44:00.004 Moke[4321:848015] 刚从休眠中被唤醒
2016-11-02 14:44:00.045 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:44:00.045 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:44:00.045 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:44:00.046 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:44:00.046 Moke[4321:848015] 即将进入到休眠状态
2016-11-02 14:44:00.264 Moke[4321:848015] 刚从休眠中被唤醒
2016-11-02 14:44:00.271 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:44:00.272 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:44:00.272 Moke[4321:848015] 即将进入到休眠状态
2016-11-02 14:44:00.272 Moke[4321:848015] 刚从休眠中被唤醒
2016-11-02 14:44:00.272 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:44:00.273 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:44:00.274 Moke[4321:848015] 即将进入到休眠状态
2016-11-02 14:44:00.274 Moke[4321:848015] 刚从休眠中被唤醒
2016-11-02 14:44:00.274 Moke[4321:848015] 即将处理 Timer 事件
2016-11-02 14:44:00.274 Moke[4321:848015] 即将处理 Source 事件
2016-11-02 14:44:00.274 Moke[4321:848015] 即将进入到休眠状态

通过 CFRunLoopObserverRef,能够监听到 RunLoop 在某个 Mode(上面是 kCFRunLoopDefaultMode)下哪些状态的改变情况(上面是 kCFRunLoopAllActivities),可以对照以下 RunLoop 的内部逻辑:

RunLoop 内部逻辑

RunLoop 内部逻辑(文字版)

具体 RunLoop 内部源码整理看 YYKit 大神 ibireme 的文章。

RunLoop 在 iOS 开发中的应用

理论知识那么多,那么 RunLoop 在我们实际开发中,有哪些应用呢?

1. NSTimer

这个不多说,上面已经有介绍了:

1
2
self.timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(startAutoScroll) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

这样可以让滚动着的 tableView 的 cell 的定时器也能运行。

2. 图片延时显示

1
2
// 只在 NSDefaultRunLoopMode 模式下显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"baobao"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];

有时候如果在 tableView 中滑动的时候显示大图片会导致卡顿,这个时候就可以用这个小 tip,让图片在 tableView 滚动时不显示(即使图片已经下载完毕),滚动停止的时候才显示。

3. Perform Selector

这张图已经诠释:

Cocoa Perform Selector Sources

4. 常驻线程

常驻线程有什么用呢?让一个一直存在的子线程,等待其他线程发来消息,处理其他事件,一般来说用于作为守护进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface ViewController ()
/** 线程对象有一个强引用,如果是局部的话,运行完任务就会销毁掉 */
@property (strong, nonatomic) NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithBlock:^{
@autoreleasepool {
// 只要往 RunLoop 中添加了 Timer、Source 或者 Observer 就会继续执行,一个 RunLoop通常必须包含一个输入源或者定时器来监听事件,如果一个都没有,RunLoop 启动后立即退出。
// 当然,在这里也可以用 NSTimer 添加到 RunLoop 中
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]; // 因为不是主线程,所以这个子线程的 RunLoop 只是创建了,但是还没有启动
}
}];
[self.thread start]; // 启动线程
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(doSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)doSomething {
// 这个线程一直存在,当需要使用此线程在处理一些事情的时候就这么调用
NSLog(@"%s, %@", __func__, [NSThread currentThread]);
}
@end

线程的创建以及销毁会消耗比较大的时间,线程一般都是在任务执行完毕之后消亡。主线程之所以一直存在,是因为有 RunLoop,所以,子线程想要一直存在,变成常驻线程,就要使用 RunLoop。

小 Tip:为什么在上面写的代码中,RunLoop 没有调用 run 方法?

1
2
self.timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(startAutoScroll) userInfo:nil repeats:YES]; // 创建一个 Timer 对象
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; // 添加 Timer 对象到 RunLoop

其实不是,主要要看这个 RunLoop 是哪个 RunLoop。[NSRunLoop currentRunLoop] 代表拿到的是当前的 RunLoop,如果当前的 RunLoop 是主线程相关的 RunLoop,那么它在程序启动的时候就已经运行了,所以不需要再调用 run 方法(无论是往这个 RunLoop 添加 Timer 也好,添加 Port 也好);如果当前的 RunLoop 是子线程的 RunLoop,如果它还没有启动,就要调用 run 方法来启动。

大家也注意到这个 autoreleasepool 关键字,为什么加上这个关键字?

5. 自动释放池(autoreleasepool)

可以将一些对象放到自动释放池里面,当这个自动释放池被释放的时候,里面的对象都会调用 Release 方法,释放这些对象。

总结:

  • 什么是 RunLoop?

    • RunLoop,从字面意思看,是 运行循环、跑圈 的意思。

    • 其实在它内部的实现就是一个 do-while 循环,在这个循环中不断的处理各种任务(比如 Source、Timer、Observer)。

    • 一个线程对应一个 RunLoop,主线程的 RunLoop 默认已经启动,子线程的 RunLoop 得手动启动(调用 run 方法)。

    • RunLoop 只能选择一个 Mode 启动,如果当前 Mode 中没有任何 Source、Timer,那么这个 RunLoop 就会启动后马上退出(注意如果仅有 Observer,这个 RunLoop 也会启动后马上退出)。

  • 自动释放池(autoreleasepool)什么时候释放?

    通过 Observer 监听 RunLoop 的状态,一旦监听到 RunLoop 即将进入到休眠状态(kCFRunLoopBeforeWaiting),就释放 autoreleasepool。

    kCFRunLoopEntry = (1UL << 0), // 创建自动释放池
    kCFRunLoopAfterWaiting = (1UL << 6), // 销毁自动释放池,创建一个新的自动释放池
    kCFRunLoopExit = (1UL << 7), // 销毁自动释放池

  • 在开发中如何使用 RunLoop?有什么应用场景?

    • 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
      • 案例 1:在子线程中开启一个定时器(RunLoop 记得要调用 run 方法)
      • 案例 2:在子线程中进行一些长期监控(一直扫描沙盒、语音监控等)
    • 可以控制定时器在特定模式下执行
    • 可以让某些事件(行为、任务)在特定模式下执行
    • 可以添加 Observer 来监听 RunLoop 的状态,比如监听点击事件的处理(在所有点击事件之前做一些处理)

记得参考:http://blog.ibireme.com/2015/05/18/runloop/

YYKit 作者 ibireme:深入理解RunLoop

ibireme | 2015-05-18 |

RunLoop 是 iOS 和 OSX 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。之后会介绍一下在 iOS 中,苹果是如何利用 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。

RunLoop 的概念

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

1
2
3
4
5
6
7
function loop() {
initialize(); // 一些初始化操作
do {
var message = get_next_message();
process_message(message); // 处理消息
} while (message != quit); // 当(消息 != quit 消息)时
}

这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

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

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

小 Tips:

  • 线程安全就是说多线程访问同一代码,不会产生不确定的结果(dispatch_once 就是)。

  • 线程安全一般都涉及到 synchronized 就是一段代码同时只能有一个线程来操作,不然中间过程可能会产生不可预制的结果。

CFRunLoopRef 的代码是开源的,你可以在这里 http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation 的源码来查看。

(Update: Swift 开源后,苹果又维护了一个跨平台的 Core Foundation 版本:https://github.com/apple/swift-corelibs-foundation/,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。)

RunLoop 与线程的关系

首先,iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread。过去苹果有份文档/tn/tn2028.html)标明了 NSThread 只是 pthread_t 的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自最底层的 mach thread。苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 pthread_t 和 NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程;也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。CFRunLoop 是基于 pthread 来管理的。

附:

  1. Mach threads represent the lowest level threading on the system.
  2. POSIX threads (pthreads) are layered on top of Mach threads.
  3. Cocoa threads (NSThreads) are layered directly on top of pthreads.
  4. Carbon MP tasks, which are API compatible with the MP tasks in Mac OS 9, are layered on top of pthreads.
  5. Carbon Thread Manager cooperative threads, which are also API compatible with their Mac OS 9 equivalent, are also layered on top of pthreads.

Mac OS X Threading

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ★注意:这里的代码只是大概的思路
/// 全局的 Dictionary,key 是 pthread_t(线程), value 是 CFRunLoopRef(RunLoop)
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
// 加锁
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局 Dictionary,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
// 这样子,Dictionary 中就有一个 key 为主线程,value 为 main loop 的元素了
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
// 然后将它放到 Dictionary 里面
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
// 解锁
OSSpinLockUnLock(&loopsLock);
return loop;
}
// 获取 main loop
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
// 获取当前 loop
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。

线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。

RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。

你只能在一个线程的内部获取其 RunLoop(因为是通过 CFRunLoopGetCurrent() 获取的,如果在主线程中获取的话,得到就是主线程的 RunLoop 了)。主线程除外,可以直接通过 CFRunLoopGetMain() 获取其 RunLoop。

RunLoop 对外的接口

在 Core Foundation 里面关于 RunLoop 有 5 个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:
RunLoop0

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef 是事件产生的地方。Source 有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1,即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2,即将处理 Timer 事件
kCFRunLoopBeforeSources = (1UL << 2), // 4,即将处理 Source 事件
kCFRunLoopBeforeWaiting = (1UL << 5), // 32,即将进入休眠状态
kCFRunLoopAfterWaiting = (1UL << 6), // 64,刚从休眠中被唤醒
kCFRunLoopExit = (1UL << 7), // 128,即将退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // A combination of all the preceding stages.
};

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop 的 Mode

CFRunLoopMode 和 CFRunLoop 的结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为 “Common” 属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为 “Common” 属性。kCFRunLoopDefaultMode 是 App 平时所处的状态,UITrackingRunLoopMode 是追踪 UIScrollView 滑动时的状态。当你创建一个 Timer 并加到 kCFRunLoopDefaultMode 时,Timer 会得到重复回调,但此时滑动一个 UITableView 时,RunLoop 会将 Mode 切换为 UITrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有 “Common” 属性的 Mode 里去。

CFRunLoop 对外暴露的管理 Mode 接口只有下面2个:

1
2
3
4
5
// Adds a mode to the set of run loop common modes.
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
// Runs the current thread’s CFRunLoop object in a particular mode.
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

Mode 暴露的管理 mode item 的接口有下面几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Adds a CFRunLoopSource object to a run loop mode.
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
// Adds a CFRunLoopObserver object to a run loop mode.
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
// Adds a CFRunLoopTimer object to a run loop mode.
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
// Removes a CFRunLoopSource object from a run loop mode.
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
// Removes a CFRunLoopObserver object from a run loop mode.
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
// Removes a CFRunLoopTimer object from a run loop mode.
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。

RunLoop 的内部逻辑

根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:

RunLoop 内部逻辑

The Run Loop Sequence of Events

Each time you run it, your thread’s run loop processes pending events and generates notifications for any attached observers. The order in which it does this is very specific and is as follows:

  1. Notify observers that the run loop has been entered.

  2. Notify observers that any ready timers are about to fire.

  3. Notify observers that any input sources that are not port based are about to fire.

  4. Fire any non-port-based input sources that are ready to fire.

  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.

  6. Notify observers that the thread is about to sleep.

  7. Put the thread to sleep until one of the following events occurs:

    • An event arrives for a port-based input source.

    • A timer fires.

    • The timeout value set for the run loop expires.
    • The run loop is explicitly woken up.
  8. Notify observers that the thread just woke up.

  9. Process the pending event.

    • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
    • If an input source fired, deliver the event.
    • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
  10. Notify observers that the run loop has exited.

其内部代码整理如下 (太长了不想看可以直接跳过去,后面会有说明):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// • 用 DefaultMode 启动 RunLoop
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// • 用指定的 Mode 启动 RunLoop,允许设置 RunLoop 超时时间以及处理完后退出
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop 的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据 modeName 找到对应 mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果 mode 里没有 source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非 port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非 port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于 port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的 Source 事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有 dispatch 到 main_queue 的 block,执行 block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于 port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到 Loop 的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入 loop 时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode 里没空,loop 也没被停止,那继续 loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop 的底层实现

从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。
RunLoop 底层实现

苹果官方将整个系统大致划分为上述 4 个层次:
应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua(如 OSX 的半透明窗口和图形特效)、SpringBoard(锁屏界面等乱七八糟的) 等。
应用框架层即开发人员接触到的 Cocoa 等框架。
核心框架层包括各种核心框架、OpenGL 等内容。
Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

我们在深入看一下 Darwin 这个核心的架构:
RunLoop Darwin

其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit(还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC(进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些 API 非常基础,如果没有这些 API 的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口(port)之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

Mach 的消息定义是在 头文件的,很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t;
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;

一条 Mach 消息实际上就是一个二进制数据包(BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:

1
2
3
4
5
6
7
8
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱(trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:

mach_msg() 函数

这些概念可以参考维基百科: System_callTrap_(computing))。

RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

关于具体的如何利用 mach port 发送信息,可以看看 NSHipster 这一篇文章,或者这里的中文翻译 。

关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian

苹果用 RunLoop 实现的功能

首先我们可以看一下 App 启动后 RunLoop 的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}
common mode items = {
// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}
// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}
// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},
modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},
CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},
CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}

可以看到,系统默认注册了5个Mode:

  1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  5. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

你可以在这里看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了。

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去 (call out), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];

定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

关于 GCD

实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

关于网络请求

iOS 中,关于网络请求的接口自下至上有如下几层:

1
2
3
4
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire

• CFSocket 是最底层的接口,只负责 socket 通信。
• CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
• NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
• NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

下面主要介绍下 NSURLConnection 的工作过程。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

RunLoop NSURLConnectionLoader

NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

RunLoop 的实际应用举例

AFNetworking

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (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 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

1
2
3
4
5
6
7
8
9
10
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
具体的代码可以看这里:_ASAsyncTransactionGroup

最后

好长时间没写博客了喵~前几天给博客搬了个家,从越来越慢的 AWS 迁到了 Linode,然后很认真的换了一套新的博客主题,排版看着还说得过去吧~ :oops: