runloop 实践相关

文章目录
  1. 1. 检测 iOS App 卡顿
    1. 1.1. 思路分析
      1. 1.1.1. 卡顿如何造成的
    2. 1.2. 卡顿监听实践
    3. 1.3. 如何获取卡顿的方法堆栈信息?
      1. 1.3.1. 直接调用系统函数获取
      2. 1.3.2. 第三方库来获取堆栈信息
  2. 2. 利用RunLoop空闲时间

RunLoop 是系统层级上的设计,用来给管理系统消息队列派发,那我们都可以用 runLoop 做什么呢?
简单来说,RunLoop 是用来监听输入源,进行调度处理的。

RunLoop 输入源可以是:

  • 输入设备
  • 网络
  • 周期性或者延迟时间
  • 异步回调

runloop activities

runloop 的 observer 可以监听的 7中状态

1
2
3
4
5
6
7
8
9
10
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 之前
kCFRunLoopBeforeSources , // 触发 Source0 之前
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息(等待源Source和计时器Timer之前,进入睡眠)
// 在这两个状态中真正处理事件
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息(等待源Source和计时器Timer后,同时在被唤醒之前)
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}

检测 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;
}

卡顿监听实践

  1. 创建 runloop 的observer对象:
1
2
3
let weakSelf = Unmanaged<Monitor>.passUnretained(self).toOpaque()
var ctx: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, weakSelf: info, retain: nil, release: nil, copyDescription: nil)
self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, runLoopCallBack(), &ctx)
  1. 将 observer 添加到 runloop 的 commonModes 中
1
CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.runLoopObserver, CFRunLoopMode.commonModes)
  1. 创建子线程,监听runloop的状态
    • beforeSources: 进入睡眠前
    • afterWaiting: 唤醒后的状态
    • 设置卡顿阀值
    • 打印堆栈信息

为什么要监听 beforeSources 和 afterWaiting 这两个状态呢?
因为只有这两个状态 runloop 触发事件回调,如果runloop 长时间处于这两个状态中说明卡顿!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DispatchQueue.global().async {
while true {
guard let sem = self.dispatchSemaphore?.wait(timeout: DispatchTime.now() + 1 / 50) else { return }
if case DispatchTimeoutResult.timedOut = sem {
guard let _ = self.runLoopObserver else {
self.dispatchSemaphore = nil
self.runLoopActivity = nil
return
}

if (self.runLoopActivity == CFRunLoopActivity.beforeSources || self.runLoopActivity == CFRunLoopActivity.afterWaiting) {
print("symbo: \(Thread.callStackSymbols)")
print("打印卡顿堆栈...")
}
}
}
}

如何获取卡顿的方法堆栈信息?

直接调用系统函数获取

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
static int s_fatal_signals[] = {
SIGABRT,
SIGBUS,
SIGFPE,
SIGILL,
SIGSEGV,
SIGTRAP,
SIGTERM,
SIGKILL,
};

static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);

void UncaughtExceptionHandler(NSException *exception) {
NSArray *exceptionArray = [exception callStackSymbols]; // 得到当前调用栈信息
NSString *exceptionReason = [exception reason]; // 非常重要,就是崩溃的原因
NSString *exceptionName = [exception name]; // 异常类型
}

void SignalHandler(int code) {
NSLog(@"signal handler = %d",code);
}

void InitCrashReport() {
// 系统错误信号捕获
for (int i = 0; i < s_fatal_signal_num; ++i) {
signal(s_fatal_signals[i], SignalHandler);
}

//oc 未捕获异常的捕获
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

int main(int argc, char * argv[]) {
@autoreleasepool {
InitCrashReport();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

第三方库来获取堆栈信息

PLCrashReporter

利用RunLoop空闲时间

卡顿是因为 runloop 一次时间 > 1/60s
那么如果 runloop 一次运行时间 < 1/60s 呢?
譬如你把手机放在那看着 app,runloop 在那睡觉(kCFRunLoopBeforeWaiting)

这个时候往 runloop 里面放个 source or timer,runloop 就会醒来 进入 kCFRunLoopAfterWaiting 状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typealias BeforeWaitingDo = () -> ()
var tasks: [BeforeWaitingDo] = []

@objc func exec() {
let t = self.tasks.remove(at: 0)
t()
}

func registerObserver() {
let rl = CFRunLoopGetCurrent()
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue, true, 0) {[weak self] (observer, actives) in
guard let self = self,
let _ = self.tasks.first else {return}
// 创建一个 source0 把 runloop 被叫醒
self.perform(#selector(ChatEmojiViewController.exec), on: Thread.current, with: nil, waitUntilDone: false)
}

CFRunLoopAddObserver(rl, observer, CFRunLoopMode.commonModes)
}

《iOS开发高手课》
iOS 性能监控(二)—— 主线程卡顿监控
优化UITableViewCell高度计算的那些事