iOS高级进修之路

iOS 利用 RunLoop 原理去监控卡顿

2022-06-30  本文已影响0人  白水灬煮一切

本文是借鉴 戴铭老师 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 状态定义的值,分别是 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);

上一篇 下一篇

猜你喜欢

热点阅读