iOS学习笔记 利用RunLoop原理去监控卡顿
造成卡顿的原因:
-
复杂UI、图文混排的绘制量过大
-
在主线程上做网络同步请求
-
在主线程上做大量的IO操作
-
运算量过大,CPU持续高占用
-
死锁和子线程抢锁
是否监视FPS来确定卡顿? 即使帧数低,页面也可能是连贯的,所以监控FPS不合理。比如动画1秒也就24张图。
RunLoop原理
监控卡顿,就是要找到主线程干了什么。线程的消息传递依赖于NSRunLoop,所以从NSRunLoop入手,就可以知道主线程调用了哪些方法。
通过监听NSRunLoop的状态,可以发现调用方法是否执行时间过长,从而判断是否会出现卡顿。
RunLoop:监听输入源,进行调度处理。会接受两种类型的输入源:另一个线程或者另一个App的异步消息 与 来自预定时间或重复间隔的同步事件。所以输入源可以是周期性(NSTimer)、延迟时间(dispatch_after)、异步回调(async或另一个App的调用)、网络、输入设备。
RunLoop的目的:当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠。
RunLoop的原理主要分七步:
-
通知observers(监听者、观察者):RunLoop要开始进入loop了,随后进入loop。
-
开启一个do while 来保活线程。通知observers:RunLoop会触发Timer回调、Sourcer0回调,接着执行加入的Block。然后触发Source0回调,如果有Source1是ready状态的话,就会跳转到handle_msg去处理消息。
-
回调触发后,通知Observers:RunLoop的线程将进入休眠(sleep)状态。
4.进入休眠后,会等待mach_port的消息,以再次唤醒。被唤醒的四个事件:
-
基于port的Source事件
-
Timer时间到
-
RunLoop超时(时间到了RunLoop还没起来干活)
-
被调用者唤醒
-
唤醒时通知Observer:RunLoop 的线程刚刚被唤醒。
-
RunLoop被唤醒后开始处理消息:
-
如果Timer时间到,就触发Timer的回调
-
如果是dispatch的话,就执行Block
-
如果是Source1事件的话,就处理这个事件
消息执行完后,执行加到loop里的block
- 根据当前RunLoop的状态判断是否走下一个loop。当被外部强制停止或loop超时,就不继续下一个loop,否则继续走下一个loop。
所以整个loop的状态包括六个:
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。
要想监听RunLoop,需要创建一个RunLoop的观察者,将创建好的观察者添加到主线程的RunLoop的common模式下观察。 然后创建一个持续的子线程专门用来监控主线程的RunLoop。
一旦监听到进入睡眠前的kCFRunLoopBeforeSources 或被唤醒后的kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。此时可dump出堆栈的消息,分析哪个方法的执行时间过长。
如何获取卡顿的方法堆栈信息?
直接调用系统函数。性能消耗小,不过只能获取简单的信息。
或者,直接使用第三方PLCrashReporter来获取堆栈信息,能获取到具体代码问题的位置,消耗也不大。
参考地址:代码
前排安利另一个聚聚写的RunLoop:深入理解RunLoop
这次想起以前遇到过的卡顿效果,真的是坑啊。
印象最深的一次卡顿:在TableView的Cell里访问了数据库。
其次是在一个答题系统里,多选单选与判断题,一个Cell从最少两个选项、一个选项三到N行,到最多九个选项。然后使用了TableViewCell的高度自动计算,后来换成了自己手动计算并缓存。
还有Cell的样式太多,后来把尽可能相同的都放到一个Cell里,也就是:一个Cell有七行,如果有四行不显示,那就移除四行,如果需要九行就再加两行。还有就是网络请求同步操作,结果数据过大,然后就做了异步和无数据的默认显示。
其实我就是从这几个方面下手的:页面重叠部分、简单的动画与显示用CALayer自己画、对象释放与创建的次数、高度自动缓存、autoRelease使用、static使用部分(如单列对象)、UI刷新前的数据处理部分、页面刷新次数、通知和监听的移除、数据的不合理读取、复杂操作尽量异步、
最后想说下:线程的消息传递依赖于NSRunLoop,函数的调用依赖runtime。感觉二者挺像的。