美文网首页iOSiOS实践ios-专题
iOS开发中的卡顿分析

iOS开发中的卡顿分析

作者: wuyou1998 | 来源:发表于2021-11-22 15:29 被阅读0次

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

    方案一:监控FPS

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

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

    #import "FPSMonitor.h"
    #import <UIKit/UIKit.h>
    
    @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 <UIKit/UIKit.h>
    #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;
    }
    
    

    整体流程如下图所示。

    事件循环机制

    根据这张图可以看出: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主线程,费电 中等

    相关文章

      网友评论

        本文标题:iOS开发中的卡顿分析

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