目前iOS中的卡顿监控的通用做法是在iOS的Runloop中进行耗时监控, 当阈值超时以后dump出卡顿的函数调用栈.
iOS中主线程的卡顿产生的原因有很多, 主线程的卡顿会导致APP的响应变差. 目前业内最好的方案都是通过Runloop来进行卡顿监控, 当阈值超时, 以后通过dump出main thread的线程调用栈.
iOS Runloop基础
用戴铭大大的一张图表示:
代码方面可以参考源码, 基本抽象如下:
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg. ==> 休眠了!!!!
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9.1 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
并且runloop有如下几个状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调
kCFRunLoopBeforeSources , // 触发 Source0 回调
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}
业内常规的方案: 基于Runloop的运行状态, 注册Observer
, 当主线程状态运行超过一定阈值, 就认为主线程卡顿了!!!
Matrix的主线程卡顿方案
Matrix的主线程卡顿方案类似:
- 在 Runloop 的起始最开始和结束最末尾位置添加 Observer,从而获得主线程的开始和结束状态。
- 当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿。
- 默认主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒。每隔 1 秒,子线程检查主线程的运行状态;
- 如果检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并获得当前的线程快照。
- 同时如果cpu使用率超过80%,也会认为是卡顿
具体的代码实现在WCBlockMonitorMgr
中:
// 必须在 mainThread 中调用
- (void)addRunLoopObserver {
NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];
// the first observer -> info 参数不会释放, 因此传入结构体的时, 直接使用 __bridge 桥接
CFRunLoopObserverContext context = { 0, (__bridge void *)self, NULL, NULL, NULL };
// order 表示观察者的顺序
// begin observer
CFRunLoopObserverRef beginObserver =
CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
CFRetain(beginObserver); // 手动管理一下内存
m_runLoopBeginObserver = beginObserver;
// 另外一个是last observer
// the last observer
CFRunLoopObserverRef endObserver =
CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
CFRetain(endObserver); // 手动管理一下内存
m_runLoopEndObserver = endObserver;
// 添加 observer
CFRunLoopRef runloop = [curRunLoop getCFRunLoop];
CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);
// for InitializationRunLoopMode
CFRunLoopObserverContext initializationContext = { 0, (__bridge void *)self, NULL, NULL, NULL };
m_initializationBeginRunloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
LONG_MIN,
&myInitializetionRunLoopBeginCallback,
&initializationContext);
CFRetain(m_initializationBeginRunloopObserver);
// 结束的 observer
m_initializationEndRunloopObserver =
CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myInitializetionRunLoopEndCallback, &initializationContext);
CFRetain(m_initializationEndRunloopObserver);
// 指定了单独的 runloopmode -> APP 启动以后运行的第一个 mode. 在启动完成以后就不再使用 -> 为了监控APP的启动耗时
// UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
CFRunLoopAddObserver(runloop, m_initializationBeginRunloopObserver, (CFRunLoopMode) @"UIInitializationRunLoopMode");
CFRunLoopAddObserver(runloop, m_initializationEndRunloopObserver, (CFRunLoopMode) @"UIInitializationRunLoopMode");
}
// 多个回调方法 -- 记录各个状态的时间戳
可以看到Matrix在Runloop中注册了多个关键的Observer, 并在关键位置记录了Runloop到达某个状态执行时间. 然后启动一个子线程, 在指定时间去判断是否卡顿!!! 如果卡顿再dump关键的主线程的线程快照 具体的逻辑如图所示:
- (void)threadProc {
...
while (YES) {
@autoreleasepool {
if (g_bMonitor) {
// 检查时间是否卡顿!
EDumpType dumpType = [self check];
if (m_bStop) {
break;
}
// 根据卡顿的类型: 1. CPU卡顿 2. 主线程卡顿 3. 线程数量超过64个
if (dumpType != EDumpType_Unlag) {
if (EDumpType_BackgroundMainThreadBlock == dumpType || EDumpType_MainThreadBlock == dumpType) {
// 认为线程太多 导致的卡顿
if (g_CurrentThreadCount > 64) {
dumpType = EDumpType_BlockThreadTooMuch;
[self dumpFileWithType:dumpType];
} else {
// 主线程卡顿 --> dump 主线程卡顿的堆栈
// ...
m_potenHandledLagFile = [self dumpFileWithType:dumpType];
}
} else if (EDumpType_CPUBlock == dumpType) {
// ... CPU 太高导致的卡顿
} else {
m_potenHandledLagFile = [self dumpFileWithType:dumpType];
}
}
// ...
}
// 定时打印主线程调用堆栈
// 时间间隔处理,检测时间间隔正常情况是1秒,间隔时间会受检测线程退火算法影响,按照斐波那契数列递增,直到没有卡顿时恢复1秒
for (int nCnt = 0; nCnt < m_nIntervalTime && !m_bStop; nCnt++) {
if (g_MainThreadHandle && g_bMonitor) {
int intervalCount = g_CheckPeriodTime / g_PerStackInterval;
if (intervalCount <= 0) {
usleep(g_CheckPeriodTime);
} else {
for (int index = 0; index < intervalCount && !m_bStop; index++) {
usleep(g_PerStackInterval); // 定时休眠
...
// 指定时间间隔 - 缓存主线程的栈帧信息
[WCGetMainThreadUtil
getCurrentMainThreadStack:^(NSUInteger pc) {
stackArray[nSum] = (uintptr_t)pc;
nSum++;
}
withMaxEntries:g_StackMaxCount
withThreadCount:g_CurrentThreadCount];
[m_pointMainThreadHandler addThreadStack:stackArray andStackCount:nSum];
}
}
} else {
usleep(g_CheckPeriodTime);
}
}
if (m_bStop) {
break;
}
}
}
}
可以看出, 方法中主要处理3个事情:
- 检测卡顿的类型
- 根据退火算法控制时间间隔
- 记录主线程堆栈
具体实现上, 可以归纳成如下过程:
- 创建的子线程通过 while 使其成为常驻线程
- 通过runloop中记录的状态时间, 在子线程中调用
check
方法判断是否卡顿 - 如果卡顿, 判断卡顿类型以及针对不同卡顿原因进行不同的处理
- 根据退火算法生成的间隔时间, update主线程的调用栈信息
卡顿时, 耗时栈帧提取
通常能想到的方式是在检测到卡顿时, dump 主线程的调用栈帧!!! 但是这种方式无法精确的收集到具体的耗时方法, 因为间隔时间存在, 导致卡顿检测到的一瞬间主线程调用栈可能并没包括耗时方法.
因此Matrix设计了一种方案, 缓存最近的连续几个调用栈快照, 当卡顿时, 比较最近的几个快照, 找到真实的耗时方法!!! 具体的逻辑可以参考Matrix-iOS 卡顿监控
简单整理一下思路:
- 卡顿监控定时获取主线程堆栈,并将堆栈保存到内存的一个循环队列中!!!
- 当主线程检测到卡顿时, 通过对保存到循坏队列中的堆栈进行回溯,获取最近最耗时堆栈. 具体的过程如下:
- 以栈顶函数为特征,认为栈顶函数相同的即整个堆栈是相同的;
- 取堆栈的间隔是相同的,堆栈的重复次数近似作为堆栈的调用耗时,重复越多,耗时越多;
- 重复次数相同的堆栈可能很有多个,取最近的一个最耗时堆栈。
- 获取到耗时堆栈以后, 通过
KSCrash
将堆栈信息dump到文件中!!!
参考
cloud.tencent.com/developer/a…