美文网首页
iOS 性能监控:Runloop 卡顿监控的坑

iOS 性能监控:Runloop 卡顿监控的坑

作者: 啸狼天 | 来源:发表于2022-03-25 09:12 被阅读0次

背景

前两天,一位朋友遇到一个问题,说自己无法使用 Runloop 监测到 -tableView:didSelectRowAtIndexPath: 场景的卡顿。

什么意思呢?就是监控不到下面这段代码的卡顿问题:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    int a = 8;
    NSLog(@"调试:大量计算");
    for (long i = 0; i < 999999999; i++){
        a = a + 1;
    }
    NSLog(@"调试:大量计算结束");
}

当用户点击 cell 时,上述代码会触发一次大量计算,具体的调用栈和调试日志如下所示:

image

从红框的 console 日志,我们可以发现上面卡顿监控代码,没有任何相关的卡顿提示

出于好奇心,我就对这个问题研究了一番,找到的原因,在这里做一次总结和分享。

常规卡顿方案和问题

首先,问题的主要原因在于目前网上的「Runloop 卡顿监控」技术方案并不是完善的,存在一些漏洞,会导致丢失这类场景的监控。

如果我们用 Runloop 卡顿 为关键字,搜索相关技术方案,基本上都是下面这种方案:

 // 注册
- (void)beginMonitor {
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    //将观察者添加到主线程runloop的common模式下的观察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
 
    //创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        int i=0;
        //子线程开启一个持续的loop用来进行监控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 80 * NSEC_PER_MSEC));
            NSLog(@"while%@",@(i++));
            printAct(self->runLoopActivity);
            if (semaphoreWait != 0) {
                if (!self->runLoopObserver) {
                    self->timeoutCount = 0;
                    self->dispatchSemaphore = 0;
                    self->runLoopActivity = 0;
                    return;
                }
                //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    //出现三次出结果
                    if (++self->timeoutCount < 3) {
                        continue;
                    }
                    NSLog(@"调试:监测到卡顿");
                } //end activity
            }// end semaphore wait
            self->timeoutCount = 0;
        }// end while
    });
}
// 记录状态
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
 
    printAct(activity);
 
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

简单的整理一下上述代码的思路:

  • 创建一个 CFRunLoopObserverRef,并提供 runLoopObserverCallBack 记录 RunloopCFRunLoopActivity 变化

  • main Runloop 添加该 observer

  • 开启异步线程,并以指定间隔持续监测 CFRunLoopActivity

  • 连续 3 次检测到 kCFRunLoopBeforeSources 或者 kCFRunLoopAfterWaiting 时,认为当前处于卡顿状态,触发 卡顿 的数据收集

卡顿监控失效分析

1、代码执行顺序

首先,我们先将监控代码与 Runloop 的执行顺序合并到一起进行分析:

  • Runloop 通知 卡顿检测代码 进入 kCFRunLoopBeforeWaiting 状态

  • Runloop 执行 UIKit点击事件 逻辑

  • Runloop 进入 休眠状态

    image

值得重点关注的是上图两个回调的执行顺序:卡顿监控点击事件 更早接收到 kCFRunLoopBeforeWaiting 事件。

点击事件 执行时,异步线程会因为 卡顿监控先接到 kCFRunLoopBeforeWaiting状态,导致错误认为 Runloop 处于睡眠状态

所以,为了解决卡顿监控 代码无法检测 tableView:didSelectRowAtIndexPath: 的现象,我们需要将 kCFRunLoopBeforeWaiting_卡顿监控 调用时机进行调整

2、 __CFRunLoopDoObservers 函数的执行逻辑

为了调整回调的执行顺序,我们需要先了解 __CFRunLoopDoObservers 函数的执行逻辑。

  
/* rl is locked, rlm is locked on entrance and exit */
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) __attribute__((noinline));
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */
    
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_START, rl, rlm, activity, 0);
    
    CHECK_FOR_FORK();
    // 获取 runLoopMode 的 observer 数量,如果小于1,则直接返回
    CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
    if (cnt < 1) return;
 
    /* Fire the observers */
    STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);
    CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
    CFIndex obs_cnt = 0;
    // 1、顺序遍历 _observers,
    // 因为每个 observer 可以观察不同的 activity,所以,需要通过 & 操作符过滤需要触发的 observer
    // 并组成新的数组 collectedObservers
    for (CFIndex idx = 0; idx < cnt; idx++) {
        CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
        // 【避免递归】1、通过 __CFRunLoopObserverIsFiring 判断是否处于执行状态
        if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
            collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
        }
    }
    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);
    // 2、顺序遍历 collectedObservers
    for (CFIndex idx = 0; idx < obs_cnt; idx++) {
        CFRunLoopObserverRef rlo = collectedObservers[idx];
        __CFRunLoopObserverLock(rlo);
        if (__CFIsValid(rlo)) {
            // 【非重复 observer】1、记录是否属于非重复 observer
            Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
            // 【避免递归】2、回调前,通过 __CFRunLoopObserverSetFiring 记录执行的状态
            __CFRunLoopObserverSetFiring(rlo);
            __CFRunLoopObserverUnlock(rlo);
            CFRunLoopObserverCallBack callout = rlo->_callout;
            void *info = rlo->_context.info;
            cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_START, callout, rlo, activity, info);
            // 3、执行 observer 的回调
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(callout, rlo, activity, info);
            cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_END, callout, rlo, activity, info);
            // 【非重复 observer】2、非重复 observer,在回调完毕后,直接销毁
            if (doInvalidate) {
                CFRunLoopObserverInvalidate(rlo);
            }
            // 【避免递归】3、回调后,通过 __CFRunLoopObserverUnsetFiring 恢复状态
            __CFRunLoopObserverUnsetFiring(rlo);
        } else {
            __CFRunLoopObserverUnlock(rlo);
        }
        CFRelease(rlo);
    }
    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);
 
    if (collectedObservers != buffer)
        free(collectedObservers);
    
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_END, rl, rlm, activity, 0);
}
 

值得注意的是,__CFRunLoopMode 持有一个数组类型的结构成员:_observers

image
  • __CFRunLoopDoObservers 会先遍历 _observers ,并根据各种条件组成一个新的数组 collectedObservers

  • 新的数组生成后,会再次遍历 collectedObservers,并通过 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 回调监控函数

所以,我们可以得到第一个重要的结论:通过控制 _observers 数组的排列顺序,能够改变调用时机

3、CFRunLoopAddObserver 函数的执行逻辑

为了控制 _observers 数组的排列顺序,我们还需要先看看 CFRunLoopAddObserver 函数的执行逻辑。

如下,创建 CFRunLoopObserverRef 时,开发者可以传入 CFIndex order 参数

CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);

image

CFRunLoopAddObserver 函数内部会根据 CFRunLoopObserverRef_order 逆序遍历 CFRunLoopRef_observers,并找到合适的位置进行插入

具体的源码如下所示:

// 添加 observer
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) {
    CHECK_FOR_FORK();
    CFRunLoopModeRef rlm;
    // 如果 runloop 处于销毁状态,直接返回
    if (__CFRunLoopIsDeallocating(rl)) return;
    // 如果主线程已经停止执行,则直接返回
    if (__CFMainThreadHasExited && rl == CFRunLoopGetMain()) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            CFLog(kCFLogLevelError, CFSTR("Attempting to add observer to main runloop, but the main thread has exited. This message will only log once. Break on _CFRunLoopError_MainThreadHasExited to debug."));
        });
        _CFRunLoopError_MainThreadHasExited();
        return;
    }
    // 合规性校验 & 防止重入
    if (!__CFIsValid(rlo) || (NULL != rlo->_runLoop && rlo->_runLoop != rl)) return;
 
    __CFRunLoopLock(rl);
    // 1、如果监听 kCFRunLoopCommonModes,则遍历 _commonModes,并进行监听
    if (modeName == kCFRunLoopCommonModes) {
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        CFSetAddValue(rl->_commonModeItems, rlo);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlo};
            /* add new item to all common-modes */
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
    } else {
        rlm = __CFRunLoopFindMode(rl, modeName, true);
        if (NULL != rlm && NULL == rlm->_observers) {
            rlm->_observers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
        }
        if (NULL != rlm && !CFArrayContainsValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo)) {
                Boolean inserted = false;
                // 2、逆序遍历 _observers,并找到合适的位置进行插入
                for (CFIndex idx = CFArrayGetCount(rlm->_observers); idx--; ) {
                    CFRunLoopObserverRef obs = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
                    if (obs->_order <= rlo->_order) {
                        CFArrayInsertValueAtIndex(rlm->_observers, idx + 1, rlo);
                        inserted = true;
                        break;
                    }
                }
                if (!inserted) {
                CFArrayInsertValueAtIndex(rlm->_observers, 0, rlo);
                }
            rlm->_observerMask |= rlo->_activities;
            __CFRunLoopObserverSchedule(rlo, rl, rlm);
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
}
 

为了方便读者理解上面的逻辑,我们通过一个具体的示例进行讲解。

如下,假设现有的 observersorder0812,新插入的 observersorder 分别是 010

则,两个 observers 会分别插入到 stub0stub1 位置。

所以,我们可以得到第二个重要结论:通过调整 CFRunLoopObserverCreateorder 参数,可以调整两个回调的执行顺序。

image

高可用的 Runloop 卡顿监测方案

根据前面的两个结论,我们可以采用将 order 调整到 LONG_MAX 的方式改变调用顺序:

1、优化方案

runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                          kCFRunLoopAllActivities,
                                          YES,
                                          LONG_MAX,
                                          &runLoopObserverCallBack,
                                          &context);

重新编译&运行APP后,我们可以发现 console 的内容变成如下:

image

2、高可用方案

相信聪明的读者很容易发现上面的优化方案仍然存在下面的badcase
kCFRunLoopAfterWaiting_其它阻塞事件 位置发生卡顿时,新方案因为执行顺序比较晚,卡顿监控代码仍然认为当前处于休眠状态,导致无法进行卡顿监控。

image

针对上面的情况,我们可以使用的Observer 的方式处理:

  • 第一个 Observerorder 调整到 LONG_MIN

    • 进入 kCFRunLoopAfterWaiting 状态时,第一个被调用,用于监控 Runloop 处于 运行状态
  • 第二个 Observerorder 调整到 LONG_MAX

    • 进入 kCFRunLoopBeforeWaiting 状态时,最后一个被调用,用于判断 Runloop 处于 睡眠状态

如下图所示,通过 Observer ,我们可以更加准确的判断Runloop 的运行状态,从而对卡顿进行更加有效的监控。

image
- (void)addRunLoopObserver
{
    NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];
 
    // 第一个监控,监控是否处于 **运行状态**
    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
    CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
    CFRetain(beginObserver);
    m_runLoopBeginObserver = beginObserver;
 
    //  第二个监控,监控是否处于 **睡眠状态**
    CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
    CFRetain(endObserver);
    m_runLoopEndObserver = endObserver;
 
    CFRunLoopRef runloop = [curRunLoop getCFRunLoop];
    CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);
    CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);
 
}
 
// 第一个监控,监控是否处于 **运行状态**
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    g_runLoopActivity = activity;
    g_runLoopMode = eRunloopDefaultMode;
    switch (activity) {
        case kCFRunLoopEntry:
            g_bRun = YES;
            break;
        case kCFRunLoopBeforeTimers:
            if (g_bRun == NO) {
                gettimeofday(&g_tvRun, NULL);
            }
            g_bRun = YES;
            break;
        case kCFRunLoopBeforeSources:
            if (g_bRun == NO) {
                gettimeofday(&g_tvRun, NULL);
            }
            g_bRun = YES;
            break;
        case kCFRunLoopAfterWaiting:
            if (g_bRun == NO) {
                gettimeofday(&g_tvRun, NULL);
            }
            g_bRun = YES;
            break;
        case kCFRunLoopAllActivities:
            break;
        default:
            break;
    }
}
 
//  第二个监控,监控是否处于 **睡眠状态**
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    g_runLoopActivity = activity;
    g_runLoopMode = eRunloopDefaultMode;
    switch (activity) {
        case kCFRunLoopBeforeWaiting:
            gettimeofday(&g_tvRun, NULL);
            g_bRun = NO;
            break;
        case kCFRunLoopExit:
            g_bRun = NO;
            break;
        case kCFRunLoopAllActivities:
            break;
        default:
            break;
    }
}

总结

本文通过分析 __CFRunLoopDoObservers 函数 和 CFRunLoopAddObserver 函数的内部逻辑,分析了网络上广泛流传的 Runloop 卡顿监测方案 存在低可用性问题的原因,并给出了一份高可用的 Runloop 卡顿监测方案

相关文章

  • iOS通过runloop监控卡顿

    质量监控-卡顿检测iOS实时卡顿监控基于Runloop简单监测iOS卡顿的demo微信iOS卡顿监控系统iOS-R...

  • iOS 性能监控:Runloop 卡顿监控的坑

    背景 前两天,一位朋友遇到一个问题,说自己无法使用 Runloop 监测到 -tableView:didSelec...

  • 卡顿检测资料

    微信iOS卡顿监控系统 卡顿方案思考 卡顿检测 移动端监控体系之技术原理 iOS性能检测

  • iOS开发中的卡顿分析

    市面上的iOS卡顿分析方案有三种:监控FPS、监控RunLoop、ping主线程。 方案一:监控FPS 一般来说,...

  • iOS性能优化-RunLoop卡顿监控

    卡顿主要表现为主线程卡死,不响应用户动作或者响应很慢,这种体验很差,会让用户对产品的认可度急速下滑,如果不及时优化...

  • Matrix-iOS 卡顿、内存监控 (一)

    Matrix-iOS 卡顿监控Matrix-iOS 内存监控 一、卡顿检测 Matrix-iOS 在addMoni...

  • iOS 性能监控(二)—— 主线程卡顿监控

    级别:★★☆☆☆标签:「iOS」「性能监控」「工具」「RunLoop」作者: 647 审校: QiShare团队...

  • 常规优化技巧

    卡顿优化 添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的...

  • 学习笔记集合01

    一、性能优化 1.0、APM性能监控 CPU占用率、内存/磁盘使用率、卡顿监控定位、Crash防护、线程数量监控、...

  • iOS中的3种卡顿检测

    市面上的iOS卡顿分析方案有三种:监控FPS、监控RunLoop、ping主线程。 前面2个都比较熟悉,第三个是最...

网友评论

      本文标题:iOS 性能监控:Runloop 卡顿监控的坑

      本文链接:https://www.haomeiwen.com/subject/oaurjrtx.html