iOS之卡顿检测方案
卡顿检测的方案根据线程是否相关分为两大类:
- 检测任务耗时
- 检测主线程是否能响应任务
与主线程相关的检测方案:
- fps
- ping
- RunLoop
与主线程不相关的检测方案:
- stack backtrace
- msgSend Observe
FPS监测
原理:
- 通常情况下,屏幕会保持
60hz/s
的刷新速度。 - 每次刷新时会发出一个屏幕刷新信号,
CADisplayLink
允许我们注册一个与刷新信号同步的回调处理。 - 记录上次计算
fps
的时间,计算本次回调的时间和上次计算时间间隔interval
,计算fps:fps = 1 / interval
- 每次计算
fps
的同时,更新上次计算fps
时间为当前时间,开始新一轮的记录。
获取fps:
1 | - (void)startFpsMonitoring { |
Ping
ping是常用的网络测试工具,用来测试数据包能否到达ip地址。
大多数手机的屏幕刷新频率是60HZ,如果在 1000/60=16.67ms 内没有将这一帧的任务执行完毕,就会发生丢帧现象,这便是用户感受到卡顿的原因。
实现思路:创建一个子线程进行循环检测,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程休眠设定的阈值,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。
主要代码:
1 | self.semaphore = dispatch_semaphore_create(0); |
美团Hertz实现思路:
检测主线程每次执行消息循环的时间,当这一时间大于阈值时,就记为发生一次卡顿。
runloop
RunLoop在BeforeSources
和AfterWaiting
后会进行任务的处理。可以在此时阻塞监控线程并设置超时时间,若超时后RunLoop的状态仍为RunLoop在BeforeSources
或AfterWaiting
,表明此时RunLoop仍然在处理任务,主线程发生了卡顿。
- 我们需要创建一个
CFRunLoopObserverContext
观察者,且创建一个Observer
,并监控主线程状态的变化 - 这个
Observer
会监听kCFRunLoopAllActivities
(所有状态改变),并在状态改变时执行runLoopObserverCallBack
中的代码。 - 创建一个子线程,使用while循环保活,并通过信号量阻塞该线程
- 如果各状态切换没有发生阻塞,那么会及时发出信号量的激活信号,代码继续执行,不视为卡顿。反之各状态耗时过长,没有及时发出信号,则视为发生卡顿。
1 | - (void)beginMonitor { |
stack backtrace
代码质量不够好的方法可能会在一段时间内持续占用CPU
的资源。
如果在一段时间内,调用栈总是停留在执行某个地址指令的状态。由于函数调用会发生入栈行为,比对两次调用栈的符号信息,如果前者是后者的符号子集时,可以认为出现了卡顿
:
1 | @interface StackBacktrace : NSThread |
msgSend observe
OC
方法的调用最终转换成objc_msgSend
的调用执行,通过在函数前后插入自定义的函数调用,维护一个函数栈结构可以获取每一个OC
方法的调用耗时。这种方式比较适合在开发阶段分析、优化代码;另外就是局限于OC语言:
1 |
|