APP卡顿检测
平时所说的“卡顿”,主要是因为在主线程执行了比较耗时的操作
卡顿的原因
- 复杂 UI 、图文混排的绘制量过大
- 在主线程上做网络同步请求
- 在主线程做大量的IO操作
- 运算量过大,CPU持续高占用
- 死锁和主子线程抢锁
RunLoop的运行逻辑:
RunLoop是用来监听输入源,进行调度处理的。这里的输入源可以是输入设备、网络、周期性或者延迟性时间、异步回调。
RunLoop会接收两种类型的输入源:
- 来自另一个线程或者来自不同应用的异步消息
- 来自订阅时间或者重复间隔的同步事件
RunLoop的目的是 当有事件处理时保持线程忙,当没有事件处理时让线程进入休眠。
通过将那些繁重而不紧急会大量占用CPU任务(比如图片加载),放到空闲的RunLoop模式里执行,就可以避开在UITrackingRunLoopMode这个RunLoop模式时是执行。
UITrackingRunLoopMode 是用户进行滚动操作时会切换到的 RunLoop 模式,避免在这个 RunLoop 模式执行繁重的 CPU 任务,就能避免影响用户交互操作上体验。
loop的六个状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调
kCFRunLoopBeforeSources , // 触发 Source0 回调
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting , // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}
如果runloop的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程,表现出来的就是出现了卡顿。
所以,如果我们要利用 RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。RunLoop 在进入睡眠之前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要触发 Source0 回调和接收 mach_port 消息两个状态。
如何检查卡顿?
检测思路:可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的.
也可以参考别人代码:
- https://github.com/UIControl/LXDAppFluecyMonitor.git
- https://github.com/ming1016/DecoupleDemo/blob/master/DecoupleDemo/SMLagMonitor.m
监听大致步骤:
- 要想监听 RunLoop,首先需要创建一个 CFRunLoopObserverContext 观察者,代码如下:
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
将创建好的观察者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
});
触发卡顿的时间阈值,我们可以根据 WatchDog 机制来设置。WatchDog 在不同状态下设置的不同时间,如下所示:
- 启动(Launch):20s;
- 恢复(Resume):10s;
- 挂起(Suspend):10s;
- 退出(Quit):6s;
- 后台(Background):3min(在 iOS 7 之前,每次申请 10min; 之后改为每次申请 3min,可连续申请,最多申请到 10min)
如何获取卡顿的方法堆栈信息:
可以借助第三方工具PLCrashReporter
地址:https://github.com/microsoft/plcrashreporter.git