主要是主线程阻塞。在开发过程当中,遇到的形成主线程阻塞的缘由多是:html
Matrix 卡顿监控在 RunLoop 的起始最开始和结束最末尾位置添加 Observer,从而得到主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态,当主线程的状态运行超过必定阈值则认为主线程卡顿,从而标记为一个卡顿。ios
采用两个准则:git
微信公开使用的卡顿监控中,主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒。每隔 1 秒,子线程检查主线程的运行状态;若是检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并得到当前的线程快照。同时,微信团队也认为 CPU 太高也可能致使应用出现卡顿,因此在子线程检查主线程状态的同时,若是检测到 CPU 占用太高,会捕获当前的线程快照保存到文件中。目前微信应用中认为,单核 CPU 的占用超过了 80%,此时的 CPU 占用就太高了。github
为了下降检测带来的性能损耗,为检测线程增长了退火算法:算法
子线程检测到主线程 Runloop 时,会得到当前的线程快照当作卡顿文件,但当前的主线程堆栈不必定是最耗时的堆栈,不必定是致使主线程超时的主要缘由。Matrix 卡顿监控经过主线程耗时堆栈提取来解决这个问题。 卡顿监控定时获取主线程堆栈,并将堆栈保存到内存的一个循环队列中。以下图,每间隔时间 t 得到一个堆栈,而后将堆栈保存到一个最大个数为 3 的循环队列中。有一个游标不断的指向最近的堆栈。 微信的策略是每隔 50 毫秒获取一次主线程堆栈,保存最近 20 个主线程堆栈。这个会增长 3% 的 CPU 占用,内存占用能够忽略不计。bash
当主线程检测到卡顿时,经过对保存到循坏队列中的堆栈进行回溯,获取最近最耗时堆栈。 以下图,检测到卡顿时,内存的循环队列中记录了最近的20个主线程堆栈,须要从中找出最近最耗时的堆栈。Matrix 卡顿监控用以下特征找出最近最耗时堆栈:微信
卡死检测流程以下网络
卡顿监控须要仔细定义本身的分类规则。能够是从调用堆栈的最外层开始归类,或者是取中间一部分归类,或者是取最里面一部分归类。各有优缺点:数据结构
微信采用了最内层归类的优化版,亦即进行二级归类。 第一级按照最内倒数2层归类,这样可以将同一缘由的卡顿集中起来; 第二级分类是从第一级点击进来,而后从最内层倒数4层进行归类,这样可以将同一缘由的不一样业务分散归类起来。ide
灰度收集到的结果是用户平均天天会产生30个 dump 文件,压缩上传大约要 300k 流量。预计正式发布的话会对后台有比较大的压力,对用户也有必定流量损耗。因此必须进行抽样上报。
很容易想到经过检测FPS就能够知道App是否发生了卡顿,也可以经过一段连续的FPS帧数计算丢帧率来衡量当前页面绘制的质量。然而实践发现FPS的刷新频率很是快,而且容易发生抖动,所以直接经过比较经过FPS来侦测卡顿是比较困难的。而检测主线程消息循环执行的时间就要容易的多了,这也是业内经常使用的一种检测卡顿的方法。所以,Hertz在实践中采用的就是检测主线程每次执行消息循环的时间,当这一时间大于阈值时,就记为发生一次卡顿。
有的卡顿连续性耗时较长,例如打开新页面时的卡顿;而有的卡顿连续性耗时相对较短但频次较快,例如列表滑动时的卡顿。所以,采用了“N次卡顿超过阈值T”的断定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:例如卡顿阈值T=2000ms、卡顿次数N=1,能够断定为单次耗时较长的卡顿;而卡顿阈值T=300ms、卡顿次数N=5,能够断定为频次较快的卡顿。
第一个问题是堆栈抓取的时机。抓取堆栈的时机必须是在卡顿发生当时,而不是以后,不然不能准确抓到形成卡顿的代码,所以在子线程中当卡顿尚未结束时抓取堆栈。 第二个问题是堆栈如何归类,卡顿堆栈的归类和Crash堆栈不一样,以最内层代码归类显然是不合适的,由于外层不一样的业务逻辑代码在最内层的调用堆栈有多是相同的。以最外层代码归类也是不合适的,由于最外层代码有多是业务逻辑代码,也有多是系统调用。采用最内层归类的原则,并匹配一些简单的规则,以命中规则的类名来归类。
检查卡顿的依据和上报时机(bugly.qq.com/docs/user-g… 依据是监控主线程 Runloop 的执行,观察执行耗时是否超过预约阀值(默认阀值为3000ms) ,若是监控到卡顿时会当即记录线程堆栈到本地,在App从后台切换到前台时,执行上报。
如何监控卡顿
MTHawkeye是美图开源的卡顿监控,看了源码设计思想跟微信的Matrix差很少,不过有不少技术的沉淀,应该是为美图定制,设计很值得研究。
业界大部分的监控方案大同小异,基于监听RunLoop的通知状态,开启常驻子线程定时检测主线程的RunLoop状态切换是否存在超时,超时则记为一次卡顿,当卡顿时长超过设定的阈值dump堆栈,进行相关策略处理以后在合适的时间上报。Matrix为腾讯最新的开源库,其堆栈处理策略较好,目前备受欢迎。
FPS(Frames Per Second)表示页面每秒的帧数,FPS越高代表页面越流畅,值50~60之间是比较流畅的,反之低于会卡顿。FPS经过借助CADisplayLink在一个周期的计数间接表示。例外,根据可滑动界面在滑动状态RunLoop由kCFRunLoopDefaultMode切换成UITrackingRunLoopMode能够区分页面不流畅产生的场景是否在滚动过程。
CADisplayLink是以跟IOS设备相同屏幕刷新频率(每秒60帧)的定时器,经过添加一个target和绑定selector,以NSRunLoopCommonModes模式将该定时器注册到RunLoop,屏幕收到每一帧的刷屏通知同时调用target绑定的selector计数操做,获取时间戳大于1秒的计数为当前页面的FPS。
实现原理:ping经常使用于测试网络测试数据包可否到达ip地址进而测试网络应答。固然用来监控卡顿监控,主要的核心思想是开启子线程维护一个ping定时器,经过固定时间片断ping主线程(发送一个通知),若是主线程不是繁忙状态会收到通知并pong回应(回送一个通知给子线程),不然子线程超过设定的pong定时阈值,没有收到主线程pong回复则断定为是卡顿了,而后dump堆栈下来。
实现原理:开启一个子线程,监听RunLoop的通知状态,若是在设定的卡顿阈值时间内没有收到RunLoop的通知状态,那么就断定为主线程卡顿了,而后dump堆栈,反之没有卡顿,能够记录卡顿的频次,到达必定的频次再上报。
oc每一个方法的调用最终都是转成objc_msgSend方式通知消息,经过维护一个数据结构统计每一个方法的调用时长进行性能分析,可是这样很是损耗性能,维护成本比较高,不推荐使用。
最后,决定采起监听RunLoop的方式,参考Matrix和MTHawkeye。
在 RunLoop 的起始最开始和结束最末尾位置添加 Observer,从而得到主线程的开始和结束状态的耗时。卡顿监控起一个子线程定时检查主线程的状态(默认200ms),当主线程的状态运行耗时超过必定阈值(默认400ms)则认为主线程卡顿,从而标记为一个卡顿。若是卡顿时长超过8秒,则断定为卡死。卡顿阈值和检测线程的周期直接影响卡顿监控的能力和性能损耗。
系统提供task_threads方法获取task的全部线程,每个线程的信息能够经过thread_get_state方法获取到,信息填充在 _STRUCT_MCONTEXT 类型的参数中,经过这个参数能够取到当前线程的Stack Pointer和Frame Pointer,而后回溯整个函数的调用栈找到全部函数的地址,经过偏移计算出物理地址,最后再进行符号化取得函数名。
采用了退火算法一部分过滤连续相同堆栈
ThreadBacktraceSnapshot *mainBacktraceSnapshot = [self generateBacktraceSnapshot:dumpType];
ThreadBacktraceSnapshot *preSnapshot = self.snapshotsArray.lastObject;
if (preSnapshot) {
if (![preSnapshot.backtraceDescription isEqualToArray:mainBacktraceSnapshot.backtraceDescription]) {
mainBacktraceSnapshot.capturedCount = self.annealingCount;
[self.snapshotsArray addObject:mainBacktraceSnapshot];
self.annealingCount = 1;
} else {
self.annealingCount += 1;
}
} else {
self.annealingCount = 1;
[self.snapshotsArray addObject:mainBacktraceSnapshot];
}
复制代码
上述的实现方案能够记录到卡住的时间,业务能够定制卡顿多长时间则断定为卡死的时间。
当页面卡顿时长超过卡死的阈值,在这个阈值的基础上在计时,直到RunLoop进入下一个状态计时结束,不然会一直等到触发Watch
/**
设定卡顿阈值和检测卡顿线程检测时间间隔启动监控
@param runloopTimeOut 卡顿阈值
@param checkRunLoopTimeOutThreshold 检测卡顿线程检测时间间隔
*/
- (void)startWithRunloopTimeOut:(useconds_t)runloopTimeOut andCheckPeriodTime:(unsigned)checkRunLoopTimeOutThreshold;
/**
采用默认值启动,卡顿阈值400ms,每隔200ms检查一次
*/
- (void)start;
- (void)stop;
复制代码
CPU波动2%-4% Dog监控阈值将APP杀死,用这种方式即便最终发生了闪退也能够逼近实际的卡死时间,偏差暂未有结论。
目前卡顿监控只是应用在demo中,没有在线上使用过,应该会有不少问题,例如性能瓶颈、堆栈过滤、退火算法优化等待。