iOS 利用 RunLoop 原理去监控卡顿
本文是借鉴 戴铭老师 iOS开发高手课 内容总结。
目录
1、卡顿问题
2、RunLoop介绍
3、RunLoop执行过程 介绍
4、RunLoop全部六个状态
5、RunLoop监控卡顿操作
6、直接用 PLCrashReporter这个开源的第三方库来获取堆栈信息
总结监控卡顿Demo:Demo
1、卡顿问题:卡顿问题,就是在主线程上无法响应用户交互的问题。如果一个 App 时不时地就给你卡一下,有时还长时间无响应
1、卡顿根源:
1>复杂 UI 、图文混排的绘制量过大;
2>在主线程上做网络同步请求;
3>在主线程做大量的 IO 操作;运算量过大,CPU 持续高占用;
4>死锁和主子线程抢锁。
2、FPS:FPS 是一秒显示的帧数,也就是一秒内画面变化数量。如果按照动画片来说,动画片的 FPS 就是 24,是达不到 60 满帧的。也就是说,对于动画片来说,24 帧时虽然没有 60 帧时流畅,但也已经是连贯的了,所以并不能说 24 帧时就算是卡住了。由此可见,简单地通过监视 FPS 是很难确定是否会出现卡顿问题了。
2、RunLoop介绍(推荐的监控卡顿的方案是:通过监控 RunLoop 的状态来判断是否会出现卡顿。)
1、RunLoop原理:对于 iOS 开发来说,监控卡顿就是要去找到主线程上都做了哪些事儿。我们都知道,线程的消息事件是依赖于 NSRunLoop 的,所以从 NSRunLoop 入手,就可以知道主线程上都调用了哪些方法。我们通过【监听 NSRunLoop 的状态,就能够发现调用方法是否执行时间过长】,从而判断出是否会出现卡顿。
2、RunLoop 是 iOS 开发中的一个基础概念,它可以做哪些事儿,以及它为什么可以做成这些事儿?
RunLoop 这个对象,在 iOS 里由 CFRunLoop 实现。
【简单来说,RunLoop 是用来监听输入源,进行调度处理的】。这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调。
RunLoop 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件。
【RunLoop 的目的是,当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠。】
所以,了解 RunLoop 原理不光能够运用到监控卡顿上,还可以提高用户的交互体验。通过将那些繁重而不紧急会大量占用 CPU 的任务(比如图片加载),放到空闲的 RunLoop 模式里执行,就可以避开在 UITrackingRunLoopMode 这个 RunLoop 模式时是执行。UITrackingRunLoopMode 是用户进行滚动操作时会切换到的 RunLoop 模式,避免在这个 RunLoop 模式执行繁重的 CPU 任务,就能避免影响用户交互操作上体验。
3、RunLoop执行过程 介绍
1、第一步通知 observers:RunLoop 要开始进入 loop 了。紧接着就进入 loop
1
2、第二步开启一个 do while 来保活线程。通知 Observers:RunLoop 会触发 Timer 回调、Source0 回调,接着执行加入的 block。代码如下:
2-0
接下来,触发 Source0 回调,如果有 Source1 是 ready 状态的话,就会跳转到 handle_msg 去处理消息。代码如下:
2-1
3、第三步回调触发后,通知 Observers:RunLoop 的线程将进入休眠(sleep)状态。代码如下:
3
4、第四步进入休眠后,会等待 mach_port 的消息,以再次唤醒。只有在下面四个事件出现时才会被再次唤醒:
1>基于 port 的 Source 事件;
2>Timer 时间到;
3>RunLoop 超时;
4>被调用者唤醒。
等待唤醒的代码如下:
4
5、第五步唤醒时通知 Observer:RunLoop 的线程刚刚被唤醒了。代码如下:
5
6、第六步RunLoop 被唤醒后就要开始处理消息了:
1>如果是 Timer 时间到的话,就触发 Timer 的回调;
2>如果是 dispatch 的话,就执行 block;
3>如果是 source1 事件的话,就处理这个事件。消息执行完后,就执行加到 loop 里的 block。代码如下:
6
7、第七步根据当前 RunLoop 的状态来判断是否需要走下一个 loop。当被外部强制停止或 loop 超时时,就不继续下一个 loop 了,否则继续走下一个 loop 。代码如下:
7
整个 RunLoop 过程,我们可以总结为如下所示的一张图片。RunLoop全部代码过程
RunLoop全部流程
4、RunLoop全部六个状态
loop 的六个状态通过对 RunLoop 原理的分析,我们可以看出在整个过程中,loop 的状态包括 6 个,其代码定义如下:
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 消息两个状态。
5、RunLoop监控卡顿操作 (参考资料)、腾讯matirx 框架 matirx 、 或者 Gitee仓库Matrix
1、开启一个子线程监控的代码如下:代码中的 NSEC_PER_SEC,代表的是触发卡顿的时间阈值,单位是秒。可以看到,我们把这个阈值设置成了 3 秒。那么,这个 3 秒的阈值是从何而来呢?这样设置合理吗?其实,触发卡顿的时间阈值,我们可以根据 WatchDog 机制来设置。WatchDog 在不同状态下设置的不同时间,如下所示:启动(Launch):20s;恢复(Resume):10s;挂起(Suspend):10s;退出(Quit):6s;后台(Background):3min(在 iOS 7 之前,每次申请 10min; 之后改为每次申请 3min,可连续申请,最多申请到 10min)。通过 WatchDog 设置的时间,我认为可以把启动的阈值设置为 10 秒,其他状态则都默认设置为 3 秒。总的原则就是,要小于 WatchDog 的限制时间。当然了,这个阈值也不用小得太多,原则就是要优先解决用户感知最明显的体验问题。
2、如何获取卡顿的方法堆栈信息?
子线程监控发现卡顿后,还需要记录当前出现卡顿的方法堆栈信息,并适时推送到服务端供开发者分析,从而解决卡顿问题。那么,在这个过程中,如何获取卡顿的方法堆栈信息呢?
获取堆栈信息的一种方法是直接调用系统函数。
这种方法的优点在于,性能消耗小。但是,它只能够获取简单的信息,也没有办法配合 dSYM 来获取具体是哪行代码出了问题,而且能够获取的信息类型也有限。这种方法,因为性能比较好,所以适用于观察大盘统计卡顿情况,而不是想要找到卡顿原因的场景。
直接调用系统函数方法的主要思路是:用 signal 进行错误信息的获取
6、直接用 PLCrashReporter这个开源的第三方库来获取堆栈信息
具体如何使用 PLCrashReporter 来获取堆栈信息,代码如下所示:
// 获取数据
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);