iOS 利用 RunLoop 原理去监控卡顿

本文是借鉴 戴铭老师 iOS开发高手课 内容总结。

目录

1、卡顿问题

2、RunLoop介绍

3、RunLoop执行过程 介绍

4、RunLoop全部六个状态

5、RunLoop监控卡顿操作 

6、直接用 PLCrashReporter这个开源的第三方库来获取堆栈信息

7、微信开源 matrix-ios卡顿监控 工具

8、腾讯 Bugly 工具 Bugly : 可监控 App在运行过程中发生的 【崩溃、卡顿、ANR、错误】

总结监控卡顿Demo:Demo

1、卡顿问题:卡顿问题,就是在主线程上无法响应用户交互的问题。如果一个 App 时不时地就给你卡一下,有时还长时间无响应

1、卡顿根源:

        1>复杂 UI 、图文混排的绘制量过大;

        2>在主线程上做网络同步请求;

        3>在主线程做大量的 IO 操作;

        4>运算量过大,CPU 持续高占用;

        5>死锁和主子线程抢锁。

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 状态定义的值,分别是 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting ,也就是要触发 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);

7、微信开源 matrix-ios卡顿监测 https://github.com/tencent/matrix/tree/master/matrix/matrix-iOS  https://github.com/Tencent/matrix 工具

微信团队就放出了一篇文章专门介绍卡顿监控方案“微信 iOS 卡顿监控系统”链接。之后,很多团队参照这篇文章开发了自己的卡顿监控系统。

1> 今年的 4 月 3 号,微信团队将他们的卡顿监控系统matrix开源出来了,包括 Matrix for iOS / MacOS https://github.com/Tencent/matrix/tree/master/matrix/matrix-iOS 和Android系统的监控方案。关于 matrix-iOS 的卡顿监控原理,你可以点击这个链接 https://github.com/Tencent/matrix/wiki/Matrix-for-iOS-macOS-卡顿监控原理 查看。

如果你的 App 现在还没有卡顿监控系统,可以考虑直接集成 matrix-iOS,直接在 Podfile 里添加 pod ‘matrix-wechat’ 就可以了。如果已经有了卡顿监控系统,我建议你阅读下 matrix-iOS 的代码,里面有很多细节值得我们学习。

比如:子线程监控检测时间间隔:matrix-iOS 监控卡顿的子线程是通过 NSThread 创建的,检测时间间隔正常情况是 1 秒,在出现卡顿情况下,间隔时间会受检测线程退火算法影响,按照斐波那契数列递增,直到没有卡顿时恢复为 1 秒。

2> 子线程监控退火算法:避免一个卡顿会写入多个文件的情况。

【为了降低检测带来的性能损耗,我们为检测线程增加了退火算法:

每次子线程检查到主线程卡顿,会先获得主线程的堆栈并保存到内存中(不会直接去获得线程快照保存到文件中);

将获得的主线程堆栈与上次卡顿获得的主线程堆栈进行比对:如果堆栈不同,则获得当前的线程快照并写入文件中;

如果相同则会跳过,并按照斐波那契数列将检查时间递增直到没有遇到卡顿或者主线程卡顿堆栈不一样。

这样,可以避免同一个卡顿写入多个文件的情况;避免检测线程遇到主线程卡死的情况下,不断写线程快照文件。】

3> RunLoop 卡顿时间阈值设置:对于 RunLoop 超时阈值的设置,微信设置的是 2 秒。CPU 使用率阈值设置:当单核 CPU 使用率超过 80%,就判定 CPU 占用过高。CPU 使用率过高,可能导致 App 卡顿。

【Matrix 卡顿监控在 Runloop 的起始 最开始和结束最末尾  位置添加 Observer,从而获得主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态,当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿。目前微信使用的卡顿监控,主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒。每隔 1 秒,子线程检查主线程的运行状态;如果检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并获得当前的线程快照。同时,我们也认为 CPU 过高也可能导致应用出现卡顿,所以在子线程检查主线程状态的同时,如果检测到 CPU 占用过高,会捕获当前的线程快照保存到文件中。目前微信应用中认为,单核 CPU 的占用超过了 80%,此时的 CPU 占用就过高了。】

4> 线程过多时 CPU 在切换线程上下文时,还会更新寄存器,更新寄存器时需要寻址,而寻址的过程还会有较大的 CPU 消耗。按照微信团队的经验,线程数超出 64 个时会导致主线程卡顿,如果卡顿是由于线程多造成的,那么就没必要通过获取主线程堆栈去找卡顿原因了。根据 matrix-iOS 的实测,每隔 50 毫秒获取主线程堆栈会增加 3% 的 CPU 占用,所以当检测到主线程卡顿以后,我们需要先判断是否是因为线程数过多导致的,而不是一有卡顿问题就去获取主线程堆栈。

你可能感兴趣的:(iOS 利用 RunLoop 原理去监控卡顿)