Wrappres' Studio.

利用 Runloop 监控卡顿

字数统计: 937阅读时长: 3 min
2019/11/03 Share

导致卡顿的原因

  • 复杂 UI 、图文混排的绘制量过大
  • 在主线程上做网络同步请求
  • 在主线程做大量的 IO 操作
  • 运算量过大,CPU 持续高占用
  • 死锁和主子线程抢锁

原理

监控卡顿就是找到主线程都做了哪些事。线程的消息事件是依赖于 NSRunLoop 的。我们通过监听 NSRunLoop 的状态,能够发现调用方法是否执行事件过长,从而判断是否会出现卡顿。
RunLoop 会接受到两种输入源,一种是来自另一个线程或者来自不同应用到异步消息,另一种是来自预定时间或者重复间隔到同步事件。RunLoop 的目的是,当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠。
Runloop 的六个状态

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调
kCFRunLoopBeforeSources , // 触发 Source0 回调
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}

如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者睡眠唤醒后接受消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如歌这个线程主线程的话,表现出来的就是出现了卡顿。
所以需要关注两个阶段。RunLoop 在进入睡眠前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting,也就是触发 Source0 回调和接受 mach_port 消息两个状态。

具体实现

创建观察者 CFRunLoopObserverContext

1
2
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);

将观察者 runLoopObserver 添加到主线程 RunLoop 到 common 模式下观察。然后创建一个持续到子线程专门用来监控主线程到 RunnLoop 状态。
一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者睡眠后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个持续的 loop 用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount = 0;
dispatchSemaphore = 0;
runLoopActivity = 0;
return;
}
//BeforeSources 和 AfterWaiting 这两个状态能够检测到是否卡顿
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
//将堆栈信息上报服务器的代码放到这里
} //end activity
}// end semaphore wait
timeoutCount = 0;
}// end while
});

获取卡顿的方法堆栈信息

直接调用系统函数

优点:性能消耗小。但只能获取简单但信息,也没办法配合 dSYM 来获取具体哪行代码出了问题,而且能够获取的信息类型也有限。

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

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 开源第三方库获取堆栈信息

能定位到具体代码,性能消耗也不大。

1
2
3
4
5
6
7
8
9
// 获取数据
NSData *lagData = [[[PLCrashReporter alloc]
initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
// 转换成 PLCrashReport 对象
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
// 进行字符串格式化处理
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
//将字符串上传服务器
NSLog(@"lag happen, detail below: \n %@",lagReportString);
CATALOG
  1. 1. 导致卡顿的原因
  2. 2. 原理
  3. 3. 具体实现
    1. 3.1. 获取卡顿的方法堆栈信息
      1. 3.1.1. 直接调用系统函数
      2. 3.1.2. 使用 PLCrashReporter 开源第三方库获取堆栈信息