美文网首页
Runloop - 卡顿检测

Runloop - 卡顿检测

作者: xxttw | 来源:发表于2023-06-26 19:08 被阅读0次

    卡顿主要表现为主线程卡死, 不响应用户操作或响应很慢, 这种体验很差, 会让用户对产品的好感地急速下滑, 如果不及时优化, 最终会导致用户流失

    • 哪些情况会导致主线程卡顿呢? 大致有如下几个方面
    1. 复杂的UI布局, 大量的图文混排绘制
    2. 复杂耗时的计算逻辑
    3. 主线程大量的IO操作
    4. 主线程同步发起网络请求
    5. 主线程死锁
    检测方案

    为了优化卡顿, 我们需要准确的知道哪里发生了卡顿, 然后才能针对性的进行优化
    检测FPS幅度其实是一种方案, 但是并不一定准确, 因为像动画片或者其他.24FPS就可以达到流程播放的程度.

    另一方方案是Runloop, 为什么Runloop可以做到卡顿监控呢, 因为我们的程序中任务都是在线程中执行, 线程又依赖于Runloop, 并且Runloop总是在相应的状态执行任务, 执行完成后就会切换到下一个状态, 如果Runloop在一个状态下执行任务的时间过长,无法进行下一个状态, 我们就可以认为发生了卡顿, 我们要做的就做检测Runloop切换状态的耗时, 多长时间可以自己控制,比方说0.5秒

    Runloop的运行状态如下
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0), // 进入Runloop
        kCFRunLoopBeforeTimers = (1UL << 1), // 处理Timer事件
        kCFRunLoopBeforeSources = (1UL << 2), // 处理Source事件
        kCFRunLoopBeforeWaiting = (1UL << 5), // 进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6), // 唤醒
        kCFRunLoopExit = (1UL << 7), // 退出Runloop
        kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
    };
    
    执行流程如下
    image.png

    在一次循环中, Timer事件, Source事件, 唤醒后的事件处理时间过长都可以认为是发生卡顿, 还有休眠之前的事件, 休眠的时间不算卡顿

    具体思路实现
    1. 我们创建一个子线程,在子线程的里监听主线程Runloop的各种状态切换
        self.ob = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0, runLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(CFRunLoopGetMain(), self.ob, kCFRunLoopCommonModes);
    
    1. 在回调函数中, 记录当前模式, 并将信号量发送出去, 以便后续进行检测任务的处理
    void runLoopObserverCallBack(CFRunLoopObserverRef observer,
                                      CFRunLoopActivity activity,
                                      void *info)
    {
        [FluecyMonitor shared].currentActivity = activity;
        dispatch_semaphore_t sema = [FluecyMonitor shared].semaphore;
        dispatch_semaphore_signal(sema);
    }
    
    1. 在子线程中开启循环检测
    • 如果当前是kCFRunLoopBeforeWaiting状态, 我们给主线程添加一个任务(timeout = NO), 子线程休眠一段时间后检测 任务是否完成, 如果没有完成说明改状态下存在卡顿情况.
    • 如果当前是其他状态, 信号量等待一定时间 任然无法获得线程资源, dispatch_semaphore_wait返回值为非0 则说明主线程runloop回调没有执行, 则也可能是发生了卡顿
    - (void)startMonitor{
        
        self.ob = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0, runLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(CFRunLoopGetMain(), self.ob, kCFRunLoopCommonModes);
        self.isMonitoring = YES;
        self.semaphore = dispatch_semaphore_create(0);
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        // 在子线程中监控
        dispatch_async(queue, ^{
            while (self.isMonitoring) {
                if (self.currentActivity == kCFRunLoopBeforeWaiting) {
                    // 处理休眠前的事件
                    __block BOOL timeout = YES;
                    // 在主线程中添加已给任务
                    dispatch_async(dispatch_get_main_queue(), ^{
                        // 主线程可以执行的话 就是告诉你他没有卡顿
                        timeout = NO;
                    });
                    //子线程休眠0.2秒 如果timeout还是YES 说明添加到主线程的任务没有被执行
                    [NSThread sleepForTimeInterval:TimeOutIntevl];
                    if (timeout) {
                        [LXDBacktraceLogger lxd_logMain];
                    }
                } else {
                    // 处理timer, source,唤醒后的事件
                    // 在执行时间内0.02 * NSEC_PER_SEC 无法获取到信号量资源.则会返回一个非0的值
                    long waitValue = dispatch_semaphore_wait(self.semaphore, 0.02 * NSEC_PER_SEC);
                    // waitValue != 0 相当于是信号量等待超时了.说明主线程runloop回调一直没有发送出来
                    if (waitValue != 0) { // 如果是超时 还一直卡在这几个状态下, 那肯定是主线程卡了
                        if (self.currentActivity == kCFRunLoopBeforeSources ||
                            self.currentActivity == kCFRunLoopBeforeTimers ||
                            self.currentActivity == kCFRunLoopAfterWaiting) {
                            [LXDBacktraceLogger lxd_logMain];
                        }
                    }
                }
            }
        });
    }
    
    1. 当Runloop 状态的切换超过了设置的阈值时, 我们就打印打钱主线程调用方法的堆栈信息,来准确的定位到底是那一个方法造成了卡顿,进而针对性的进行优化
    - (void)tableView: (UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath {
        sleep(2.0);
    }
    2022-12-31 00:22:58.343287+0800 RunLoop卡顿监控[13273:946345] 主线程卡顿 Backtrace of Thread 259:
    ======================================================================================
    libsystem_kernel.dylib         0x1cc0594c4 __semwait_signal + 8
    libsystem_c.dylib              0x1800ff774 nanosleep + 216
    libsystem_c.dylib              0x1800ff570 sleep + 48
    RunLoop卡顿监控            0x100a6becc -[ViewController tableView:didSelectRowAtIndexPath:] + 84
    UIKitCore                      0x184fb4854 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:deselectPrevious:] + 1620
    UIKitCore                      0x184fb41e8 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 112
    UIKitCore                      0x184fb4ad0 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 316
    UIKitCore                      0x185294438 -[_UIAfterCACommitBlock run] + 64
    UIKitCore                      0x18529490c -[_UIAfterCACommitQueue flush] + 188
    UIKitCore                      0x184d97a0c _runAfterC
    
    1. 我们在cell的点击事件里加入了休眠了2秒.造成了Runloop状态切换超过了阈值, 所以自动检测到了卡顿信息,我们可以根据信息修复卡顿问题

    相关文章

      网友评论

          本文标题:Runloop - 卡顿检测

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