iOS并发编程之Runloop

同学,听过while(1){}吗?在iOS系统中存在这么一套线程事件驱动模型——Runloop,用于高效的循环处理事件。接下来让我们从官方文档开始深入学习Runloop的一些话题。

在iOS系统中系统默认为主线程开启了Runloop,而其他线程默认不开启Runloop事件循环机制。

Runloop的设计细节

在Runloop中有两类的事件触发源:输入源(Input Sources)和定时器源(Timer Sources)。

从上面这张图可发现细分下来就这几种事件触发源:

1
2
3
4
5
6
Input Sources:
CFMutableSetRef _sources0 // 非基于Port的自定义源与Cocoa Perform Selector源
CFMutableSetRef _sources1 // 基于Port的触发源,监视应用程序的Mach端口

Timer Sources:
定时器触发源Timer的时间并非精确时间,受当前RunloopMode是否支持该Timer Source以及当前Runloop是否在执行其他操作影响。

Runloop依赖于这些事件触发源进行工作,当有事件的时候唤醒线程处理,没有事件的时候睡眠线程,这就是它的基本工作原理,同时Runloop还有多种工作模式(Runloop Modes)。iOS系统中允许线程在多种Runlop工作模式下工作,不同的场景可能会切换到不同的Runloop工作模式下,同一时刻下只能运行在一种工作模式下,接下来我们就继续探究Runloop Modes的一些问题。

Runloop中的工作模式Runloop Modes

从CoreFoundation的RunloopMode源码看,每一个工作模式内部主要都包含三部分:输入触发源(source0、source1) + 观察者(observers) + 定时器触发源(timers)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
mach_port_t _timerPort;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
HANDLE _timerPort;
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
};

-w300

其中触发源就是之前提到用来驱动Runloop工作的对象,而我们可以利用观察模式观测每一个工作模式下Runloop的工作细节状态(这个在后面细节分析)。在官方文档中对外公开的Core Foundation / Cocoa中5种工作模式:

而在API层面上iOS中对外声明出来的只有kCFRunLoopDefaultModekCFRunLoopCommonModes这两种工作模式的字符串,同时对外公开的Runloop Mode管理也只暴露了下面这两个接口:

1
2
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

而Runloop Mode内部的触发源/监听者管理的接口有下面几个:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

可以看到Apple并不希望开发者过多得介入到Runloop的工作细节中,我们只能够通过Runloop Mode的名称字符串来操作/监听在这个工作模式下的Runloop。如同在MTHawkeye中的ANR模块通过@"UIInitializationRunLoopMode"kCFRunLoopCommonModes这两个字符串监听这两个工作状态下Runloop的工作细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)registerObserver {
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};

if (!self.highPriorityObserverRef) {
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &mthanr_runLoopHighPriorityObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
self.highPriorityObserverRef = observer;
}
....
if (!self.highPriorityInitObserverRef) {
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &mthanr_runLoopHighPriorityObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, (CFRunLoopMode) @"UIInitializationRunLoopMode");
self.highPriorityInitObserverRef = observer;
}
...
}

每个Runloop Mode中的工作循环细节

对于开发者们,系统留给我们了Observer用于监听Runloop的运行时候的关键状态切换。在一个Runloop Mode的工作循环中会经历以下几个阶段:

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 进入
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timers
kCFRunLoopBeforeSources = (1UL << 2),// 即将处理Source0
kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 唤醒了
kCFRunLoopExit = (1UL << 7), // 退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

具体的代码我就不贴了,这里借用一张总结归纳得很好的流程图:

可以看到在同一个Runloop工作循环的周期跑下来会受到代码执行效率的影响,所以我们通常认为NSTimer这种基于Runloop实现的Timer是不精准的,但是如果在这个工作循环中的工作负担并不十分严重的时候,姑且可以认为定时器计时有效。

应用场景——侦测App的卡顿/卡死/OOM情况

接下来从MTHawkeye的ANR Trace模块学一学如何利用Runloop来侦测App运行过程中出现的卡顿/卡死情况。

这个模块的基本设计思路:通过子线程检测主线程的Runloop各个环节以达到监控主线程的工作是否正常。

当子线程触发检测(默认每0.1s触发一次)的时候通过比对当前时间戳与Runloop一次工作循环的起始时间戳判断主线程是否处于了Runloop卡顿(默认认为Runloop工作循环事件 > 0.4s即为卡顿),若果是卡顿则开始生成卡顿快照(主线程堆栈信息),就这样子直到Runloop恢复正常的时候这之后,子线程发现目前存在Record则开始将卡顿快照匹配到Record中补足卡顿信息并回调出去给外部,让它们获知目前出现卡顿了。

其中子线程负责生成主线程卡顿快照记录,Runloop负责生成卡顿Records。实际上我们认为Runloop一次工作循环的时间间隔大于卡顿时间才是真正的卡顿状态,而子线程这边的判断并不是完全精确的,所以这里子线程只用于补充卡顿记录信息。

目前我们认为在主线程的Runloop中最低优先级观察者的即将进入休眠kCFRunLoopBeforeWaiting或者即将退出目前这个Runloop模式kCFRunLoopExit的时候Runloop是非工作状态,而最高优先级观察者的其他时候是工作状态。这里我们在每一次Runloop从非工作状态更新到工作状态的时候刷新它这次循环的起始时间戳。当处于非工作状态开始判断这一次循环是不是运行超时了(卡顿),如果卡顿则新增一个Record标记这个Runloop卡顿的基本时间信息,最后在子线程里面把数据补全再上报。

这是这个模块判断卡顿的主要逻辑,那么应该如何认为App是被卡死或者OOM崩溃的呢?这里Hawkeye针对这个问题的改进受到了如何检测 iOS app 卡顿导致的系统强杀这篇文章的启发,编写了MTHANRTracingBufferRunner这个模块用于完整地记录了运行时候一小段区间内的App状态和发生卡顿时候的堆栈记录并且通过mmap写到文件中。

在下次app打开的时候可以通过读取最近一次记录来判断区分卡死、OOM这两种异常,其中
isAppStillActiveTheLastMoment表示没有收到正常的应用退出通知,可能是因为崩溃/卡死用户主动关闭/OOM等异常退出。需要结合isDuringHardStall字段判断是否为卡死,当它为true时,表示上一次应用退出的原因就是卡死导致的(可能被用户手动关闭)。这里可以参考MTHawkeye-ANR的说明文档

参考

Hawkeye - ANR Tracer
深入理解RunLoop
理解iOS Runloop
RunLoop F.A.Q
iOS RunLoop详解
微信iOS卡顿监控系统