简介
iOS卡顿监控有多种不同的方案和指标,在实施成本和指标精确度上各有不同。本文例举一下常见的方案和代码,以及其指标的实现和拟合程度。
FPS
帧率
FPS数据,依据页面信息做聚合,或者APP版本之间比对,通常用于页面性能衡量。
单一的FPS数据难以用于卡顿的判定。
掉帧率
当前主流设备屏幕普遍在60帧,新iOS设备和部分Android设备有很多在90和120帧,计算掉帧率有更好的扩展性。
帧率波动
对于游戏等APP的卡顿监控,虽然任天堂Switch只有30帧的锁帧,但是玩家也不会感觉到卡顿,但是过快的帧率波动却会导致肉眼可见的卡顿。
帧率过低当然会导致卡顿,但是帧率波动过快也是导致卡顿的原因。我们可以从帧间耗时,与前几帧帧间耗时的对比计算,得到帧率波动水平。
同时满足两条件,则认为是一次卡顿Jank.
①Display FrameTime>前三帧平均耗时2倍。
②Display FrameTime>两帧电影帧耗时 (1000ms/24*2=84ms)。
同时满足两条件,则认为是一次严重卡顿BigJank.
①Display FrameTime >前三帧平均耗时2倍。
②Display FrameTime >三帧电影帧耗时(1000ms/24*3=125ms)。
场景化
低FPS在一些场景下,才能表示页面正在发生卡顿,在页面静止的情况下,低FPS值并不能代表卡顿。
主要场景
-
页面转场
-
Scrollview等滑动
-
其他
- 容器内滑动
- 视频播放
监听页面生命周期的状态,观察页面是否处于转场中。
监听Runloop状态,观察页面是否处于滑动中。
Runloop
基于Runloop的卡顿监控,主要是判断除Runloop Waiting外的activity执行是否超出阈值时间。
iOS-Matrix中阈值时间默认值为2秒,卡顿检测间隔为1秒。
实现方案
- Observer
- Ping-Pong
Hitch Time Ratio
Instruments中当前使用该指标衡量卡顿,是指卡顿的时间占总时间的占比
量化卡顿的两种方式:
- 卡顿时间:某一帧展示的时间比预期的时间晚了多少
- 卡顿率:总的卡顿时间/总时间
标准推荐:
Critical >= 10ms/s
Warning 5..10ms/s
Good < 5ms/s
渲染流程
把显示分为了提交和渲染2个阶段
辅助信息
页面信息
获取页面栈顶部的页面,部分特殊页面需要做处理
@objc open class dynamic func topMost(of viewController: UIViewController?) -> UIViewController? {
// presented view controller
if let presentedViewController = viewController?.presentedViewController {
return topMost(of: presentedViewController)
}
// UITabBarController
if let tabBarController = viewController as? UITabBarController,
let selectedViewController = tabBarController.selectedViewController {
return topMost(of: selectedViewController)
}
// UINavigationController
if let navigationController = viewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return topMost(of: visibleViewController)
}
// UIPageController
if let pageViewController = viewController as? UIPageViewController,
pageViewController.viewControllers?.count == 1 {
return topMost(of: pageViewController.viewControllers?.first)
}
return viewController
}
堆栈信息
堆栈信息需要做符号化解析,才能从函数地址解析成函数符号。可以参考我的其他文章。
由于卡顿监控的滞后性,发现卡顿发生的时刻,堆栈可能已经变化。通常堆栈的获取并不能直接使用卡顿发生时刻的堆栈,需要使用循环队列等方式,从之前记录的一组堆栈中计算最近最耗时函数所在的堆栈。
最近最耗时函数
通过计算之前一组堆栈中重复次数最多的栈顶函数,找到其所在的堆栈,才能正确反应卡顿发生时的堆栈信息。
火焰图
获取之前发生的一组堆栈信息,把主线程堆栈组上报到对应平台,即可把完成火焰图显示出来。火焰图可以可视化的显示最近最耗时函数信息,而且并非一定需要是最近最耗时的栈顶函数。
问题
iOS常见卡顿问题总结
- NSUserDefaults:低内存的情况下,系统拷贝内存数据到磁盘
- I/O:文件读写、数据库操作
- 系统接口:获取mac地址、切换音频设备、读取桌面icon未知数
- 死锁以及锁的滥用
- 排版绘制:sizeWithFont、[EAGLContext setCurrentContext:]
- GCD并发队列短时间创建大量任务
- UIWebview初始化、销毁(使用性能更优的WKWebView)