引发卡顿的几种原因
- 复杂UI、图文混排的绘制量过大
- 在主线程上做网络同步请求
- 在主线程做大量的IO操作
- 运算量过大,导致CPU持续高占用
- 列锁和主子线程抢锁
对于卡顿监控,可能第一个想到的办法就是利用CADisplayLink
去检测FPS
值的变化,但这种方式不能满足我们对卡顿监控的需求。例如说,对于动画片来说,它的FPS
是24,远远没有达到60的满帧,但是对于动画片来说,我们已经看的是很连贯的了,感觉不出来卡顿的现像,但24帧如果出现在App上,那可想而知,几乎卡的我们想杀掉App进程了吧。
其实用户所感知的卡顿主要就是来自主线程上的卡顿,因为我们要监控解决的也是这种情况。
利用RunLoop来监控卡顿
我们都知道,线程的消息事件是依赖于NSRunLoop
的,所以从RunLoop
入手,就能知道主线程上都做了哪些事,通过监听RunLoop
状态,就能够发现调用方法是否执行时间过长,从而可以推断出是否出现了卡顿。
RunLoop
主要的工作就是,有事要去处理时,保持线程忙,没事时让线程进入休眠。
RunLoop
的状态
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
回调和接收match_port
消息两个状态)
监控的实现
首先需要创建一个RunLoop
的观察者,方便我们检测RunLoop
的状态
CFRunLoopObserverContext context = { 0, (__bridge void *)self, NULL, NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
再将观察者runLoopObserver
添加到主线程的RunLoop
的common
模式下,然后再创建一个持续的子线程专门用来监控主线程的RunLoop
状态
一旦发现状态为进入睡眠前的kCFRunLoopBeforeSources
,或者唤醒后的kCFRunLoopAfterWaiting
,如果在设置的时间内没有变化,即可判断为卡顿,然后dump
出堆栈的信息,然后做进一步的分析
开启一个子线程监控:
// 创建子线程监控
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
});