iOS中的3种卡顿检测

市面上的iOS卡顿分析方案有三种:监控FPS、监控RunLoop、ping主线程

前面2个都比较熟悉,第三个是最近才了解到的。

方案一:监控FPS

一般来说,我们约定60FPS即为流畅。那么反过来,如果App在运行期间出现了掉帧,即可认为出现了卡顿。

监控FPS的方案几乎都是基于CADisplayLink实现的。简单介绍一下CADisplayLink:CADisplayLink是一个和屏幕刷新率保持一致的定时器,一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector。
可以通过向RunLoop中添加CADisplayLink,根据其回调来计算出当前画面的帧数。

#import "FPSMonitor.h"
#import  
@interface FPSMonitor ()
@property (nonatomic, strong) CADisplayLink* link;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTime;
@end 
@implementation FPSMonitor 
- (void)beginMonitor {   
 _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsInfoCaculate:)];  
  [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];    
} 

- (void)fpsInfoCaculate:(CADisplayLink *)sender {  
  if (_lastTime == 0) {    
    _lastTime = sender.timestamp;   
     return;   
 }   
 _count++;   
 double deltaTime = sender.timestamp - _lastTime;  
  if (deltaTime >= 1) {    
    NSInteger FPS = _count / deltaTime;    
    _lastTime = sender.timestamp;     
   _count = 0;      
  NSLog(@"FPS: %li", (NSInteger)ceill(FPS + 0.5)); 
   }
} 
@end

FPS的好处就是直观,小手一划后FPS下降了,说明页面的某处有性能问题。坏处就是只知道这是页面的某处,不能准确定位到具体的堆栈。


方案二:监控RunLoop

首先来介绍下什么是RunLoop。RunLoop是维护其内部事件循环的一个对象,它在程序运行过程中重复的做着一些事情,例如接收消息、处理消息、休眠等等。

所谓的事件循环,就是对事件/消息进行管理,没有消息时,休眠线程以避免资源消耗,从用户态切换到内核态。

有事件/消息需要进行处理时,立即唤醒线程,回到用户态进行处理。

#import 
#import "AppDelegate.h"
 int main(int argc, char * argv[]) {  
  NSString * appDelegateClassName;   
 @autoreleasepool {    
    appDelegateClassName = NSStringFromClass([AppDelegate class]);   
 }   
 return UIApplicationMain(argc, argv, nil, appDelegateClassName);}

UIApplicationMain函数内部会启动主线程的RunLoop,使得iOS程序持续运行。

iOS系统中有两套API来使用RunLoop,NSRunLoop(CFRunLoopRef的封装)和CFRunLoopRef。Foundation框架是不开源的,可以通过开源的CoreFoundation来分析RunLoop内部实现。

点此下载CoreFoundation。

RunLoop对象底层就是一个CFRunLoopRef结构体,内部数据如下:

struct __CFRunLoop {  
  pthread_t _pthread;   // 与RunLoop一一对应的线程   
 CFMutableSetRef _commonModes;   // 存储着NSString(mode名称)的集合  
  CFMutableSetRef _commonModeItems; // 存储着被标记为commonMode的Source0/Source1/Timer/Observer  
  CFRunLoopModeRef _currentMode;   // RunLoop当前的运行模式   
 CFMutableSetRef _modes;    // 存储着RunLoop所有的 Mode(CFRunLoopModeRef)模式    // 其他属性略 };
struct __CFRunLoopMode {   
 CFStringRef _name;       // mode 类型,如:NSDefaultRunLoopMode    
CFMutableSetRef _sources0;    // 事件源 sources0  
  CFMutableSetRef _sources1;    // 事件源 sources1   
 CFMutableArrayRef _observers; // 观察者   
 CFMutableArrayRef _timers;    // 定时器      
  // 其他属性略};

Source0被添加到RunLoop上时并不会主动唤醒线程,需要手动去唤醒。Source0负责对触摸事件的处理以及performSeletor:onThread:

Source1具备唤醒线程的能力,使用的是基于Port的线程间通信。Source1负责捕获系统事件,并将事件交由Source0处理。

struct __CFRunLoopSource {    
CFRuntimeBase _base;   
 uint32_t _bits;    
pthread_mutex_t _lock;    
CFIndex _order;         /* immutable */    
CFMutableBagRef _runLoops;  
  union {              
  CFRunLoopSourceContext version0;      // 表示 sources0     
   CFRunLoopSourceContext1 version1;     // 表示 sources1  
  } _context;};

__CFRunLoopTimer和NSTimer是免费桥接toll-free bridged的。
performSelector:WithObject:afterDelay:方法会创建timer并添加到RunLoop中。

struct __CFRunLoopTimer {   
 CFRuntimeBase _base;  
  uint16_t _bits;  
  pthread_mutex_t _lock; 
   CFRunLoopRef _runLoop;   
 CFMutableSetRef _rlModes;   
 CFAbsoluteTime _nextFireDate;  
  CFTimeInterval _interval;       /* immutable */   
 CFTimeInterval _tolerance;          /* mutable */   
 uint64_t _fireTSR;          /* TSR units */  
  CFIndex _order;         /* immutable */   
 CFRunLoopTimerCallBack _callout;    /* immutable */   
 CFRunLoopTimerContext _context; /* immutable, except invalidation */};

RunLoopObserver用于监听RunLoop的六种状态。CFRunLoopObserver中的_activities用于保存RunLoop的活动状态,当状态发生改变时,通过回调函数_callout函数通知所有observer。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {  
  kCFRunLoopEntry = (1UL << 0),          // 即将进入 RunLoop   
 kCFRunLoopBeforeTimers = (1UL << 1),   // 即将处理 Timers   
 kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Sources    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠  
  kCFRunLoopAfterWaiting = (1UL << 6),   // 刚从休眠中唤醒  
  kCFRunLoopExit = (1UL << 7),           // 即将退出 RunLoop   
 kCFRunLoopAllActivities = 0x0FFFFFFFU  // 以上所有状态};
struct __CFRunLoopObserver {  
  CFRuntimeBase _base;   
 pthread_mutex_t _lock;  
  CFRunLoopRef _runLoop;   
 CFIndex _rlCount;  
  CFOptionFlags _activities;      /* immutable */   
 CFIndex _order;         /* immutable */   
 CFRunLoopObserverCallBack _callout; /* immutable */   
 CFRunLoopObserverContext _context;  /* immutable, except invalidation */};

简单过一下RunLoop的源码。

void CFRunLoopRun(void) {  
 /* DOES CALLOUT */   
 int32_t result;   
 do {     
   result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);      
  CHECK_FOR_FORK();  
  } while (
kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

简单来看RunLoop是个 do..while循环,下面来看看循环中具体干了哪些事情。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */    CHECK_FOR_FORK();  
  if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;    __CFRunLoopLock(rl);  
  //根据modeName来查找本次运行的mode   
 CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);    // 如果没找到mode 或者 mode里没有任何的事件,就此停止,不再循环  
  if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {      
       Boolean did = false;       
      if (currentMode) __CFRunLoopModeUnlock(currentMode);             __CFRunLoopUnlock(rl);      
       return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;  
  }  
  CFRunLoopModeRef previousMode = rl->_currentMode;  
  rl->_currentMode = currentMode;   
 int32_t result = kCFRunLoopRunFinished;    // 通知 observers 即将进入RunLoop  
  if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);  // RunLoop具体要做的事情 
   result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);  // 通知 observers 即将退出RunLoop 
   if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);     
    __CFRunLoopModeUnlock(currentMode);   
     __CFRunLoopPopPerRunData(rl, previousPerRun);  
  rl->_currentMode = previousMode; 
   __CFRunLoopUnlock(rl);  
  return result;}

从上面可以看到RunLoop除了通知observers即将进入/退出外,其他具体要做的事情都写在了__CFRunLoopRun中。

static int32_t __CFRunLoopRun(
CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {  
  uint64_t startTSR = mach_absolute_time();     // 状态判断  
  if (__CFRunLoopIsStopped(rl)) {   
     __CFRunLoopUnsetStopped(rl);  
  return kCFRunLoopRunStopped;  
  } else if (rlm->_stopped) {  
  rlm->_stopped = false;   
 return kCFRunLoopRunStopped;   
 }  // 初始化timeout_timer代码 略    
  int32_t retVal = 0;    
  do {      
    __CFPortSet waitSet = rlm->_portSet;    
    __CFRunLoopUnsetIgnoreWakeUps(rl);                // 通知 observers 即将处理Timer   
     if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);        // 通知 observers 即将处理Sources 
       if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);                // 处理主队列异步的block          __CFRunLoopDoBlocks(rl, rlm);                // 处理Source0     
   Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);        
if (sourceHandledThisLoop) {         
   // 处理block           
 __CFRunLoopDoBlocks(rl, rlm);  
  }        
 Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);         didDispatchPortLastTime = false;        
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {    
      // 判断有无Source1        
    if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {        
      // 有Source1就跳转到handle_msg      
          goto handle_msg;         
   }       
 }  // 通知 observers 即将进入休眠  
  if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);        __CFRunLoopSetSleeping(rl);    
    __CFPortSetInsert(dispatchPort, waitSet);   
       __CFRunLoopModeUnlock(rlm);    
      __CFRunLoopUnlock(rl);     
    CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();   
      if (kCFUseCollectableAllocator) {      
      memset(msg_buffer, 0, sizeof(msg_buffer));   
     }          
    msg = (mach_msg_header_t *)msg_buffer;  
    // 休眠,等待消息来唤醒线程    
    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);    
     __CFRunLoopLock(rl);  
      __CFRunLoopModeLock(rlm);     
    rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));   
      __CFPortSetRemove(dispatchPort, waitSet);                __CFRunLoopSetIgnoreWakeUps(rl);  
   __CFRunLoopUnsetSleeping(rl);   
   //通知 observers RunLoop刚从休眠中唤醒  
  if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))  __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 跳转标志 handle_msg        handle_msg:;   
     __CFRunLoopSetIgnoreWakeUps(rl);     
    if (MACH_PORT_NULL == livePort) {     
       CFRUNLOOP_WAKEUP_FOR_NOTHING();      
      // handle nothing   
     } else if (livePort == rl->_wakeUpPort) {     
       CFRUNLOOP_WAKEUP_FOR_WAKEUP();   
     } #if USE_MK_TIMER_TOO      // 被Timer唤醒     
   else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {    
        CFRUNLOOP_WAKEUP_FOR_TIMER();          //处理Timer        
    if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {       
         // Re-arm the next timer       
         __CFArmNextTimerInMode(rlm, rl);  
          }   
     }#endif 
       // 被GCD唤醒     
   else if (livePort == dispatchPort) {       
     CFRUNLOOP_WAKEUP_FOR_DISPATCH();    
        __CFRunLoopModeUnlock(rlm);        
    __CFRunLoopUnlock(rl);        
    _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);     
     // 处理GCD            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);            
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);         
   __CFRunLoopLock(rl);     
       __CFRunLoopModeLock(rlm);         
   sourceHandledThisLoop = true;     
       didDispatchPortLastTime = true;    
    } else {     
     // 被Source1唤醒    
        CFRUNLOOP_WAKEUP_FOR_SOURCE();   
         voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);       
     CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);      
      _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);                    }   
  // 处理Block    
    __CFRunLoopDoBlocks(rl, rlm);    
        // 处理返回值  
  if (sourceHandledThisLoop && stopAfterHandle) {   
  // 进入loop时参数标记为处理完事件就返回    
    retVal = kCFRunLoopRunHandledSource; 
 } else if (timeout_context->termTSR < mach_absolute_time()) {  
   // 超出传入参数标记的超时时间     
       retVal = kCFRunLoopRunTimedOut; 
   }
 else if (__CFRunLoopIsStopped(rl)) {   
  // 被外部调用者强行停止       
     __CFRunLoopUnsetStopped(rl);   
     retVal = kCFRunLoopRunStopped;  
  } else if (rlm->_stopped) {   
  // 自动停止    
    rlm->_stopped = false;  
      retVal = kCFRunLoopRunStopped;  
  } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {  
   // mode为空,没有source0、source1、timer、observers     
   retVal = kCFRunLoopRunFinished;  
  }       
     } while (0 == retVal);  
   if (timeout_timer) { 
       dispatch_source_cancel(timeout_timer);  
      dispatch_release(timeout_timer); 
   } else {    
    free(timeout_context);   
 }  
   return retVal;
} 

整体流程如下图所示。

9fa0581d50860693665a513c88fae399.png.jpeg

事件循环机制

根据这张图可以看出:RunLoop在BeforeSources和AfterWaiting后会进行任务的处理。可以在此时阻塞监控线程并设置超时时间,若超时后RunLoop的状态仍为RunLoop在BeforeSources或AfterWaiting,表明此时RunLoop仍然在处理任务,主线程发生了卡顿。

- (void)beginMonitor {    self.dispatchSemaphore = dispatch_semaphore_create(0);    // 第一个监控,监控是否处于 运行状态    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};    self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,                                                        kCFRunLoopAllActivities,                                                        YES,                                                        LONG_MIN,                                                        &myRunLoopBeginCallback,                                                        &context);    //  第二个监控,监控是否处于 睡眠状态    self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,                                                      kCFRunLoopAllActivities,                                                      YES,                                                      LONG_MAX,                                                      &myRunLoopEndCallback,                                                      &context);    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);        // 创建子线程监控    dispatch_async(dispatch_get_global_queue(0, 0), ^{        //子线程开启一个持续的loop用来进行监控        while (YES) {            long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));            if (semaphoreWait != 0) {                if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {                    self.timeoutCount = 0;                    self.dispatchSemaphore = 0;                    self.runLoopBeginActivity = 0;                    self.runLoopEndActivity = 0;                    return;                }                // 两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿                if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||                    (self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {                    // 出现三次出结果                    if (++self.timeoutCount < 2) {                        continue;                    }                    NSLog(@"调试:监测到卡顿");                } // end activity            }// end semaphore wait            self.timeoutCount = 0;        }// end while    });} // 第一个监控,监控是否处于 运行状态void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {    RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;    lagMonitor.runLoopBeginActivity = activity;    dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;    dispatch_semaphore_signal(semaphore);} //  第二个监控,监控是否处于 睡眠状态void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {    RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;    lagMonitor.runLoopEndActivity = activity;    dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;    dispatch_semaphore_signal(semaphore);}

方案三:Ping主线程

Ping主线程的核心思想是向主线程发送一个信号,一定时间内收到了主线程的回复,即表示当前主线程流畅运行。没有收到主线程的回复,即表示当前主线程在做耗时运算,发生了卡顿。

目前昆虫线上使用的就是这套方案。

self.semaphore = dispatch_semaphore_create(0);
- (void)main { 
   //判断是否需要上报  
  __weak typeof(self) weakSelf = self; 
   void (^ verifyReport)(void) = ^() {   
     __strong typeof(weakSelf) strongSelf = weakSelf;    
    if (strongSelf.reportInfo.length > 0) {     
       if (strongSelf.handler) {     
           double responseTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000); 
               double duration = responseTimeValue - strongSelf.startTimeValue;     
           if (DEBUG) {      
              NSLog(@"卡了%f,堆栈为--%@", duration, strongSelf.reportInfo);    
            }          
      strongSelf.handler(@{    
                @"title": [InsectUtil dateFormatNow].length > 0 ? [InsectUtil dateFormatNow] : @"",                    @"duration": [NSString stringWithFormat:@"%.2f",duration],                    @"content": strongSelf.reportInfo       
                            });   
         }          
  strongSelf.reportInfo = @"";     
   }   
 };        while (!self.cancelled) {    
    if (_isApplicationInActive) {       
     self.mainThreadBlock = YES;       
     self.reportInfo = @"";        
    self.startTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);    
        dispatch_async(dispatch_get_main_queue(), ^{          
      self.mainThreadBlock = NO;      
          dispatch_semaphore_signal(self.semaphore);    
        });        
    [NSThread sleepForTimeInterval:(self.threshold/1000)];    
        if (self.isMainThreadBlock) {         
       self.reportInfo = [InsectBacktraceLogger insect_backtraceOfMainThread];     
       }         
   dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);     
       //卡顿超时情况;          
  verifyReport();   
     } else {    
        [NSThread sleepForTimeInterval:(self.threshold/1000)];     
   }  
  }} 

总结

方案 优点 缺点 实现复杂性
FPS 直观 无法准确定位卡顿堆栈 简单
RunLoop Observer 能定位卡顿堆栈 不能记录卡顿时间,定义卡顿的阈值不好控制 复杂
Ping Main Thread 能定位卡顿堆栈,能记录卡顿时间 一直ping主线程,费电 中等

转载:https://blog.csdn.net/u014600626/article/details/122524652

你可能感兴趣的:(iOS中的3种卡顿检测)