Matrix-iOS之卡顿监控梳理

3,733 阅读6分钟

目前iOS中的卡顿监控的通用做法是在iOS的Runloop中进行耗时监控, 当阈值超时以后dump出卡顿的函数调用栈.

iOS中主线程的卡顿产生的原因有很多, 主线程的卡顿会导致APP的响应变差. 目前业内最好的方案都是通过Runloop来进行卡顿监控, 当阈值超时, 以后通过dump出main thread的线程调用栈.

iOS Runloop基础

用戴铭大大的一张图表示:

img

代码方面可以参考源码, 基本抽象如下:

{
    /// 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的主线程卡顿方案类似:

  1. 在 Runloop 的起始最开始和结束最末尾位置添加 Observer,从而获得主线程的开始和结束状态。
  2. 当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿。
  3. 默认主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒。每隔 1 秒,子线程检查主线程的运行状态;
  4. 如果检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并获得当前的线程快照。
  5. 同时如果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关键的主线程的线程快照 具体的逻辑如图所示:

image.png


- (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个事情:

  1. 检测卡顿的类型
  2. 根据退火算法控制时间间隔
  3. 记录主线程堆栈

具体实现上, 可以归纳成如下过程:

  1. 创建的子线程通过 while 使其成为常驻线程
  2. 通过runloop中记录的状态时间, 在子线程中调用check方法判断是否卡顿
  3. 如果卡顿, 判断卡顿类型以及针对不同卡顿原因进行不同的处理
  4. 根据退火算法生成的间隔时间, update主线程的调用栈信息

卡顿时, 耗时栈帧提取

通常能想到的方式是在检测到卡顿时, dump 主线程的调用栈帧!!! 但是这种方式无法精确的收集到具体的耗时方法, 因为间隔时间存在, 导致卡顿检测到的一瞬间主线程调用栈可能并没包括耗时方法.

因此Matrix设计了一种方案, 缓存最近的连续几个调用栈快照, 当卡顿时, 比较最近的几个快照, 找到真实的耗时方法!!! 具体的逻辑可以参考Matrix-iOS 卡顿监控

简单整理一下思路:

  1. 卡顿监控定时获取主线程堆栈,并将堆栈保存到内存的一个循环队列中!!!
  2. 当主线程检测到卡顿时, 通过对保存到循坏队列中的堆栈进行回溯,获取最近最耗时堆栈. 具体的过程如下:
    1. 以栈顶函数为特征,认为栈顶函数相同的即整个堆栈是相同的;
    2. 取堆栈的间隔是相同的,堆栈的重复次数近似作为堆栈的调用耗时,重复越多,耗时越多;
    3. 重复次数相同的堆栈可能很有多个,取最近的一个最耗时堆栈。
  3. 获取到耗时堆栈以后, 通过KSCrash将堆栈信息dump到文件中!!!

参考

进阶:iOS 性能优化系列

Matrix-iOS

cloud.tencent.com/developer/a…

cloud.tencent.com/developer/a…

juejin.cn/post/684490…

blog.ibireme.com/2015/05/18/…

time.geekbang.org/column/arti…