RunLoop 是系统层级上的设计,用来给管理系统消息队列派发,那我们都可以用 runLoop 做什么呢?
简单来说,RunLoop 是用来监听输入源,进行调度处理的。
RunLoop 输入源可以是:
- 输入设备
- 网络
- 周期性或者延迟时间
- 异步回调
runloop 的 observer 可以监听的 7中状态
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
检测 iOS App 卡顿
思路分析
卡顿如何造成的
iOS系统页面刷新频率:60 FPS,60次/s, let refresh_time_per = 1s/60 < 0.02
如果在 refresh_time_per
时间内没有完成图片绘制,那么就会出现卡顿现象!
而系统页面刷新事件处理…… 事件几乎都是由 runloop 调用执行的。
那么如果 runloop 一次循环时间 > refresh_time_per
就说明图片没有渲染完成,导致卡顿。
问题来了,如何判断 runloop 一次循环时间 > refresh_time_per
呢?
RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。
所以要利用 RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。
RunLoop 的两个 loop 状态
在进入睡眠之前: kCFRunLoopBeforeSources
在进入唤醒之后: kCFRunLoopAfterWaiting
也就是要触发 Source0 回调和接收 mach_port 消息两个状态。
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 // while
// 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);
}
}
// 退出 runloop 逻辑 retVal != 0 exit
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;
}
卡顿监听实践
- 创建 runloop 的observer对象:
1 | let weakSelf = Unmanaged<Monitor>.passUnretained(self).toOpaque() |
- 将 observer 添加到 runloop 的 commonModes 中
1 | CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.runLoopObserver, CFRunLoopMode.commonModes) |
- 创建子线程,监听runloop的状态
- beforeSources: 进入睡眠前
- afterWaiting: 唤醒后的状态
- 设置卡顿阀值
- 打印堆栈信息
为什么要监听 beforeSources 和 afterWaiting 这两个状态呢?
因为只有这两个状态 runloop 触发事件回调,如果runloop 长时间处于这两个状态中说明卡顿!
1 | DispatchQueue.global().async { |
如何获取卡顿的方法堆栈信息?
直接调用系统函数获取
1 | static int s_fatal_signals[] = { |
第三方库来获取堆栈信息
利用RunLoop空闲时间
卡顿是因为 runloop 一次时间 > 1/60s
那么如果 runloop 一次运行时间 < 1/60s 呢?
譬如你把手机放在那看着 app,runloop 在那睡觉(kCFRunLoopBeforeWaiting
)
这个时候往 runloop 里面放个 source or timer,runloop 就会醒来 进入 kCFRunLoopAfterWaiting
状态
1 | typealias BeforeWaitingDo = () -> () |