飞道的博客

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

426人阅读  评论(0)

作者:酷酷的哀殿,百度 iOS 开发工程师

背景

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

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


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

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

image-02214033496

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

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

常规卡顿方案和问题

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

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


   
  1.   // 注册
  2. - (void)beginMonitor {
  3.     CFRunLoopObserverContext context = { 0,(__bridge void*)self,NULL,NULL};
  4.     CFRunLoopObserverRef runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
  5.                                               kCFRunLoopAllActivities,
  6.                                               YES,
  7.                                                0,
  8.                                               &runLoopObserverCallBack,
  9.                                               &context);
  10.      //将观察者添加到主线程runloop的common模式下的观察中
  11.     CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
  12.      //创建子线程监控
  13.     dispatch_async(dispatch_get_global_queue( 00), ^{
  14.          int i= 0;
  15.          //子线程开启一个持续的loop用来进行监控
  16.         while (YES) {
  17.             long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW,  80 * NSEC_PER_MSEC));
  18.             NSLog(@ "while%@",@(i++));
  19.             printAct(self->runLoopActivity);
  20.              if (semaphoreWait !=  0) {
  21.                  if (!self->runLoopObserver) {
  22.                     self->timeoutCount =  0;
  23.                     self->dispatchSemaphore =  0;
  24.                     self->runLoopActivity =  0;
  25.                      return;
  26.                 }
  27.                  //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
  28.                  if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
  29.                      //出现三次出结果
  30.                      if (++self->timeoutCount <  3) {
  31.                          continue;
  32.                     }
  33.                     NSLog(@ "调试:监测到卡顿");
  34.                 }  //end activity
  35.             } // end semaphore wait
  36.             self->timeoutCount =  0;
  37.         } // end while
  38.     });
  39. }
  40. // 记录状态
  41. static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
  42.     SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
  43.     lagMonitor->runLoopActivity = activity;
  44.     printAct(activity);
  45.     dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
  46.     dispatch_semaphore_signal(semaphore);
  47. }

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

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

  • main Runloop 添加该 observer

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

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

卡顿监控失效分析

1、代码执行顺序

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

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

  • Runloop 执行 UIKit点击事件 逻辑

  • Runloop 进入 休眠状态

    image-17000108309

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

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

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

2、 __CFRunLoopDoObservers 函数的执行逻辑

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


   
  1. /* rl is locked, rlm is locked on entrance and exit */
  2. static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) __attribute__((noinline));
  3. static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) {  /* DOES CALLOUT */
  4.     
  5.     cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_START, rl, rlm, activity,  0);
  6.     
  7.     CHECK_FOR_FORK();
  8.      // 获取 runLoopMode 的 observer 数量,如果小于1,则直接返回
  9.     CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) :  0;
  10.      if (cnt <  1return;
  11.      /* Fire the observers */
  12.     STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <=  1024) ? cnt :  1);
  13.     CFRunLoopObserverRef *collectedObservers = (cnt <=  1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
  14.     CFIndex obs_cnt =  0;
  15.      // 1、顺序遍历 _observers,
  16.      // 因为每个 observer 可以观察不同的 activity,所以,需要通过 & 操作符过滤需要触发的 observer
  17.      // 并组成新的数组 collectedObservers
  18.      for (CFIndex idx =  0; idx < cnt; idx++) {
  19.         CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
  20.          // 【避免递归】1、通过 __CFRunLoopObserverIsFiring 判断是否处于执行状态
  21.          if ( 0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
  22.             collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
  23.         }
  24.     }
  25.     __CFRunLoopModeUnlock(rlm);
  26.     __CFRunLoopUnlock(rl);
  27.      // 2、顺序遍历 collectedObservers
  28.      for (CFIndex idx =  0; idx < obs_cnt; idx++) {
  29.         CFRunLoopObserverRef rlo = collectedObservers[idx];
  30.         __CFRunLoopObserverLock(rlo);
  31.          if (__CFIsValid(rlo)) {
  32.              // 【非重复 observer】1、记录是否属于非重复 observer
  33.             Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
  34.              // 【避免递归】2、回调前,通过 __CFRunLoopObserverSetFiring 记录执行的状态
  35.             __CFRunLoopObserverSetFiring(rlo);
  36.             __CFRunLoopObserverUnlock(rlo);
  37.             CFRunLoopObserverCallBack callout = rlo->_callout;
  38.             void *info = rlo->_context.info;
  39.             cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_START, callout, rlo, activity, info);
  40.              // 3、执行 observer 的回调
  41.             __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(callout, rlo, activity, info);
  42.             cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_END, callout, rlo, activity, info);
  43.              // 【非重复 observer】2、非重复 observer,在回调完毕后,直接销毁
  44.              if (doInvalidate) {
  45.                 CFRunLoopObserverInvalidate(rlo);
  46.             }
  47.              // 【避免递归】3、回调后,通过 __CFRunLoopObserverUnsetFiring 恢复状态
  48.             __CFRunLoopObserverUnsetFiring(rlo);
  49.         }  else {
  50.             __CFRunLoopObserverUnlock(rlo);
  51.         }
  52.         CFRelease(rlo);
  53.     }
  54.     __CFRunLoopLock(rl);
  55.     __CFRunLoopModeLock(rlm);
  56.      if (collectedObservers != buffer)
  57.         free(collectedObservers);
  58.     
  59.     cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_END, rl, rlm, activity,  0);
  60. }

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

image-17000157559
  • __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-17000220812

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

具体的源码如下所示:


   
  1. // 添加 observer
  2. void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) {
  3.     CHECK_FOR_FORK();
  4.     CFRunLoopModeRef rlm;
  5.      // 如果 runloop 处于销毁状态,直接返回
  6.      if (__CFRunLoopIsDeallocating(rl))  return;
  7.      // 如果主线程已经停止执行,则直接返回
  8.      if (__CFMainThreadHasExited && rl == CFRunLoopGetMain()) {
  9.         static dispatch_once_t onceToken;
  10.         dispatch_once(&onceToken, ^{
  11.             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."));
  12.         });
  13.         _CFRunLoopError_MainThreadHasExited();
  14.          return;
  15.     }
  16.      // 合规性校验 & 防止重入
  17.      if (!__CFIsValid(rlo) || (NULL != rlo->_runLoop && rlo->_runLoop != rl))  return;
  18.     __CFRunLoopLock(rl);
  19.      // 1、如果监听 kCFRunLoopCommonModes,则遍历 _commonModes,并进行监听
  20.      if (modeName == kCFRunLoopCommonModes) {
  21.         CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
  22.          if (NULL == rl->_commonModeItems) {
  23.             rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault,  0, &kCFTypeSetCallBacks);
  24.         }
  25.         CFSetAddValue(rl->_commonModeItems, rlo);
  26.          if (NULL != set) {
  27.             CFTypeRef context[ 2] = {rl, rlo};
  28.              /* add new item to all common-modes */
  29.             CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
  30.             CFRelease(set);
  31.         }
  32.     }  else {
  33.         rlm = __CFRunLoopFindMode(rl, modeName,  true);
  34.          if (NULL != rlm && NULL == rlm->_observers) {
  35.             rlm->_observers = CFArrayCreateMutable(kCFAllocatorSystemDefault,  0, &kCFTypeArrayCallBacks);
  36.         }
  37.          if (NULL != rlm && !CFArrayContainsValue(rlm->_observers, CFRangeMake( 0, CFArrayGetCount(rlm->_observers)), rlo)) {
  38.                 Boolean inserted =  false;
  39.                  // 2、逆序遍历 _observers,并找到合适的位置进行插入
  40.                  for (CFIndex idx = CFArrayGetCount(rlm->_observers); idx--; ) {
  41.                     CFRunLoopObserverRef obs = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
  42.                      if (obs->_order <= rlo->_order) {
  43.                         CFArrayInsertValueAtIndex(rlm->_observers, idx +  1, rlo);
  44.                         inserted =  true;
  45.                          break;
  46.                     }
  47.                 }
  48.                  if (!inserted) {
  49.                 CFArrayInsertValueAtIndex(rlm->_observers,  0, rlo);
  50.                 }
  51.             rlm->_observerMask |= rlo->_activities;
  52.             __CFRunLoopObserverSchedule(rlo, rl, rlm);
  53.         }
  54.          if (NULL != rlm) {
  55.             __CFRunLoopModeUnlock(rlm);
  56.         }
  57.     }
  58.     __CFRunLoopUnlock(rl);
  59. }

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

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

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

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

image-17000244565

高可用的 Runloop 卡顿监测方案

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

1、优化方案


   
  1. runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
  2.                                           kCFRunLoopAllActivities,
  3.                                           YES,
  4.                                           LONG_MAX,
  5.                                           &runLoopObserverCallBack,
  6.                                           &context);

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

image-03013224495

2、高可用方案

相信聪明的读者很容易发现上面的优化方案仍然存在下面的badcase

kCFRunLoopAfterWaiting_其它阻塞事件 位置发生卡顿时,新方案因为执行顺序比较晚,卡顿监控代码仍然认为当前处于休眠状态,导致无法进行卡顿监控。

image-17000304903

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

  • 第一个 Observer 的  order 调整到 LONG_MIN

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

  • 第二个 Observerorder  调整到 LONG_MAX

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

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

image-17000342009

   
  1. - (void)addRunLoopObserver
  2. {
  3.     NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];
  4.      // 第一个监控,监控是否处于 **运行状态**
  5.     CFRunLoopObserverContext context = { 0, (__bridge void *) self, NULL, NULL, NULL};
  6.     CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
  7.     CFRetain(beginObserver);
  8.     m_runLoopBeginObserver = beginObserver;
  9.      //  第二个监控,监控是否处于 **睡眠状态**
  10.     CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
  11.     CFRetain(endObserver);
  12.     m_runLoopEndObserver = endObserver;
  13.     CFRunLoopRef runloop = [curRunLoop getCFRunLoop];
  14.     CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);
  15.     CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);
  16. }
  17. // 第一个监控,监控是否处于 **运行状态**
  18. void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
  19. {
  20.     g_runLoopActivity = activity;
  21.     g_runLoopMode = eRunloopDefaultMode;
  22.      switch (activity) {
  23.          case kCFRunLoopEntry:
  24.             g_bRun = YES;
  25.              break;
  26.          case kCFRunLoopBeforeTimers:
  27.              if (g_bRun == NO) {
  28.                 gettimeofday(&g_tvRun, NULL);
  29.             }
  30.             g_bRun = YES;
  31.              break;
  32.          case kCFRunLoopBeforeSources:
  33.              if (g_bRun == NO) {
  34.                 gettimeofday(&g_tvRun, NULL);
  35.             }
  36.             g_bRun = YES;
  37.              break;
  38.          case kCFRunLoopAfterWaiting:
  39.              if (g_bRun == NO) {
  40.                 gettimeofday(&g_tvRun, NULL);
  41.             }
  42.             g_bRun = YES;
  43.              break;
  44.          case kCFRunLoopAllActivities:
  45.              break;
  46.          default:
  47.              break;
  48.     }
  49. }
  50. //  第二个监控,监控是否处于 **睡眠状态**
  51. void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
  52. {
  53.     g_runLoopActivity = activity;
  54.     g_runLoopMode = eRunloopDefaultMode;
  55.      switch (activity) {
  56.          case kCFRunLoopBeforeWaiting:
  57.             gettimeofday(&g_tvRun, NULL);
  58.             g_bRun = NO;
  59.              break;
  60.          case kCFRunLoopExit:
  61.             g_bRun = NO;
  62.              break;
  63.          case kCFRunLoopAllActivities:
  64.              break;
  65.          default:
  66.              break;
  67.     }
  68. }

总结

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

完整代码,可以访问 腾讯 的 matrix[1] 仓库获取

推荐阅读

✨ iOS 性能优化:优化 App 启动速度

✨ iOS 性能优化:使用 MetricKit 2.0 收集数据

✨ iOS 性能优化:用电池和性能 API 识别性能趋势

✨ iOS 性能优化:用 Xcode Organizer 诊断性能问题

✨ iOS 性能优化:优化 App 的持久化策略

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。

参考资料

[1]

matrix: https://github.com/Tencent/matrix/blob/master/matrix/matrix-iOS/Matrix/WCCrashBlockMonitor/CrashBlockPlugin/Main/BlockMonitor/WCBlockMonitorMgr.mm#L815-L844


转载:https://blog.csdn.net/sinat_35969632/article/details/111351356
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场