本文参考以下文章,做了一点优化,提升了卡顿监测的准确性,性能,符号化速度等等。
iOS实时卡顿监控,深入理解RunLoop,iOS版微信界面卡顿监测方案,深入剖析 iOS 性能优化,BSBacktraceLogger
UI卡顿检测
通过监测主runloop循环次数来判断是否发送卡顿。
什么是runloop?
runloop就是线程里的一个循环,退出该循环的条件是程序结束,程序什么时候结束自己定,类似MFC中消息循环。
为什么要有runloop?
线程保活,事件分发,线程频繁创建销毁耗资源,不希望线程频繁创建销毁。
怎么监控主runloop循环次数?
runloop循环的过程中会抛通知出来,在异步线程中创建一个runloop观察者监听这些通知即可。
怎么检测UI发生了卡顿?
监控主runloop循环次数,流畅情况下,一般循环60次,对应60帧。
假设认为掉了10帧,人眼能明显感受到卡顿,10 * 16.67 ms 约 166 ms,那么runloop超过166ms没回调通知给我的观察者,判定为卡顿,并且要“非常非常及时”获取下主线程的调用栈,栈顶的方法就是发生卡顿的方法。
看下图:这是网友根据runloop源码画的流程图
起一个线程用信号量卡着,每166ms执行一次,用个变量last记录最后一次runloop抛出来通知,如果发生166ms超时,去看 last 是什么值,如果是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting,表示自己的代码发生卡顿,因为这两个通知后面处理的是 source0系统代码,source1用户代码,timer代码事件。发生了卡顿就在监控线程将其调用栈获取下来。
另外页面切换速度,FPS帧率,都跟主循环正相关,因为viewDidLoad等等事件都在主线程执行,UI也在主线程绘制。监测了主runloop,这两个指标其实可以不用在监测。
符号化
符号化就是给一个内存地址 0x00001234 找到其符号 -[ViewController viewDidLoad] 的过程,因为mach-o文件中包含了LC_SYMTAB段,该段中包含符号表。
注意,iOS系统做了优化,系统库的符号不在内存中,会提示 <redacted>
符号化参考BSBacktraceLogger所写
改进的地方有:
1.预处理所有image,记录下image中所需要的各个segment基址,image内存地址范围等等,避免每次用个for循环来查找segment基址,预处理后查找基址从 O(N) 降到 O(1),注意点:image可以动态加载,动态删除,好在iOS中不会删除,只会在APP启动时会逐渐加载,加完后image数量不会变化,如果image数量有变,那么得重新预处理一次。
2.查找一个内存地址 address 在哪一个 image 内,返回该 image 索引
由于有预处理image地址范围,并对地址排了序,qsort()排序,并且image地址不重叠,那么这里查找一个image直接用二分查找,从原来的两个for循环的O(n^2)降到了O(log n),为什么不用哈希查找是因为地址空间太大,64位下有2^64个地址,大约 16777216T,太大了存不下。
3.加缓存,缓存 (address, symbol) 地址到symbol符号结构体的二元组
使用自己实现的LRU缓存,比NSCache快4倍,文章地址:https://www.jianshu.com/p/1f8e36285539
4.监控线程只获取调用栈,另起一个线程进行符号化,相当于监控线程是生产者,其他线程是消费者,一对一生产消费模型。
优化结果:1000次符号化调用
7个栈:
优化后:50ms,,,优化前:1800ms
70个栈:
优化后:800ms,,,优化前:11800ms
那么除以1000就是一次符号化的时间,大约是 0.05ms 到 0.8ms 之间能获取到全部调用栈,提高了准确性。因为在50ms发生了卡顿,该卡顿可能在51ms消失,调用栈变化十分快,必须要在最短时间内捕捉到调用栈,才能准确,如果要在几毫秒后才捕捉到,那可能就不是发生卡顿的调用栈了,导致结果不准。
优化后的代码暂时未贴出,以后会考虑开源的。
线上UI卡顿监控结果
UI卡顿监控SDK终于上线了,灰度范围5万个用户,线上 0 崩溃。
后台已捕捉到几千处方法卡顿和卡顿调用栈(卡顿定义:UI线程超过500ms没响应判定为卡顿)
卡顿主要原因是:有代码在主线程同步请求网络,主线程读写数据库,写文件,加锁,做数据解析,数据计算等等。
修复完这些卡顿,APP流畅性将得到提升,用户体验提升。
监控SDK本身的性能消耗:(iPhone 6s, iOS 9.2 测得)
SDK启动时间:0.05 ms
SDK启动后:CPU 接近 0%,内存大约几十kb
包体积:72 kb
磁盘使用:3 kb,(一次启动数据上报流量 2 kb)
常驻线程:启动一分钟内有3个,收集完数据后,常驻线程仅1个,CPU 接近 0%
另外我还一个业务型的SDK在月活1.1亿的APP上,崩溃数只有11个。