深入理解RunLoop

深入理解RunLoop

什么是 RunLoop,RunLoop的应用范畴,RunLoop内部实现逻辑,RunLoop与线程的关系,timer与RunLoop的关系,RunLoop是怎么响应用户操作的,RunLoop的几种状态,RunLoop的mode作用是什么?

什么是RunLoop

顾名思义,RunLoop是运行循环的意思,就是在程序运行中循环做一些事情。

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {
@autoreleasepool {
// 如果没有runloop,程序执行到这里就结束退出了
return 0;
// 因为有runloop,程序才能保持运行
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

RunLoop的基本作用:保持程序的持续运行、处理App中的各种事件(eg:触摸事件、定时器事件)、节省CPU资源,提高程序性能:有事做做事,没事做休息

RunLoop的应用范畴

RunLoop主要应用场景有以下几个:
1> 定时器(Timer)、performSelector
2> GCD Asyc Main Queue
3> 事件响应、手势识别、界面刷新
4> 网络请求
5> AutoreleasePool

RunLoop 对象

iOS中有2套API来访问和使用RunLoop:
1> Foundation : NSRunLoop
2> Core Foundation : CFRunLoopRef

1
2
3
4
5
6
// 两种获取 RunLoop 方式
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop];

CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
CFRunLoopRef mainRunloop2 = CFRunLoopGetMain();

NSRunLoopCFRunLoopRef都代表着RunLoop对象,NSRunLoop是基于CFRunLoop的一层OC封装

CFRunLoopRef是开源的,点击查看

RunLoop与线程

1> 每条线程都有位移的一个与之对应的RunLoop对象
2> RunLoop保存在一个全局的Dictionary,线程作为key,RunLoop作为对象
3> 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建,主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
4> RunLoop会在线程结束时销毁

源码分析

打开刚才下载的源码,我们发现CF的库是没有xcode项目的。新建一个项目,讲所有文件拖进来。

搜索RunLoop.c文件,CFRunLoopGetMain(void)函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
...
// 如果不存在,create
if (NULL != rlm && NULL == rlm->_sources0) {
...
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
// 方便理解,省略一些代码,感兴趣的同学可以深入研究
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
...
return loop;
}

里面调的是_CFRunLoopGet0(pthread_t t)这个函数,内部调用的(CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));,这个函数,根据方法名,我们可以猜测,应该是个字典,传入的参数是一个pthreadPointer线程指针,返回一个runloop对象,也证实了刚才所述的字典管理猜想。

RunLoop相关的类

Core Foundation中关于RunLoop的5个类
1> CFRunLoopRef
2> CFRunLoopModeRef
3> CFRunLoopSourceRef
4> CFRunLoopTimerRef
5> CFRunLoopObserverRef

CFRunLoopRef底层结构:

1
2
// CFRunLoopRef 底层是这种类型结构体
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

继续搜索 struct __CFRunLoop,会发现搜索出来很多,一个小技巧快速定位,在后面加一个空格和大括号:struct __CFRunLoop {

1
2
3
4
5
6
7
8
9
10
struct __CFRunLoop {
// 实际结构体成员比这个多,将几个重点的选出:
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
// 当前模式
CFRunLoopModeRef _currentMode;
// _models 存储了多个 CFRunLoopModeRef 对象,只有一个为 CurrentMode
CFMutableSetRef _modes;
};

我们发现一个RunLoop对象里面有一个pthread_t线程,所以每个线程都有一个与之对应的RunLoop;CFMutableSetRef上层与之对应NSMutableSet,与NSMutableArray用法类似,区别是数组有序,可以使用index进行取值;集合无序,取值只能通过[set anyObject]方式。

CFRunLoopModeRef底层结构是什么样子

跟刚才小技巧一样,我们直接搜索struct __CFRunLoopMode {

1
2
3
4
5
6
7
8
9
10
11
12
13
struct __CFRunLoopMode {
...
// 同样将本文关心之外的省略
// mode 名称
CFStringRef _name;
// _sources0/_sources0 集合存储CFRunLoopSourceRef对象
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
// _observers 集合存储 CFRunLoopObserverRef 对象
CFMutableArrayRef _observers;
// _timers 存储 CFRunLoopTimerRef 对象
CFMutableArrayRef _timers;
};

总结:
1> CFRunLoopModeRef代表着RunLoop的运行模式
2> 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
3> RunLoop启动时只能选择其中一个Mode,作为currentMode
4> 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。这样做的好处是不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响,系统一个时刻只处理一种mode下面的事件
5> 如果一个Mode里面没有任何的Source0/Source1/Timer/Observer,RunLoop会立马退出
如下图:
RunLoop结构

常用的Mode

1> kCFRunLoopDefaultMode (NSDefaultRunLoopMode):App默认Mode,通常主线程是在这个Mode下运行

2> UITrackingRunLoopMode:界面跟踪Mode,用于Scrollow追踪触摸滑动,保证界面在滑动时不受其他Mode影响

3> kCFRunLoopCommonModes 通用模式,默认包含上面两种模式

Source0/Source1/Timers/Obvers处理什么事件

RunLoop结构
断点touchesBegan,打印堆栈,我们发现,时间调用从Source0开始,到最后的touchesBegan

Source0:
1> 触摸事件处理
2> performSelector:onThread:

Source1:
1> 基于Port的线程间通信
2> 系统时间捕捉(eg.上面的点击事件虽然最后是Source0处理的,但是最开始其实是Source1来进行捕捉的,然后分发给Source0来进行处理)

Timers:
1> NSTimer
2> performSelector:withObject:afterDelay:

Observers:
1> 用于监听RunLoop的状态
2> UI刷新(BeforeWaiting,在休眠之前刷新UI)
3> Autorelease pool(BeforeWaiting,休眠之前将池内需要操作的对象进行一次release操作)

RunLoop几种状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即将进入 Loop
kCFRunLoopEntry = (1UL << 0),
// 即将处理 Timer
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理 Source
kCFRunLoopBeforeSources = (1UL << 2),
// 即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 刚从休眠中唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 即将退出 Loop
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

监听 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
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

// 创建 Observer
#if 1
// C 函数方式
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observerRunLoopActivities, NULL);
#elif 0
// block 方式
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

});
#endif
// 添加 Observer
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
}

void observerRunLoopActivities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
case kCFRunLoopAllActivities:
NSLog(@"kCFRunLoopAllActivities");
break;
default:
break;
}
}