iOS RunLoop - 卡顿检测

1 卡顿原因及解决方案

可能的原因:

  1. 长时间的主线程同步任务,例如大量数据的计算、I/O 操作或网络请求
  2. 复杂UI布局,例如图文混排
  3. 资源竞争,多个线程同时访问共享资源时,如果没有合适地加锁或使用其他同步机制
  4. 主线程被其他高优先级任务占用时,例如系统任务或后台任务

解决方案:

  • 使用 Instruments 工具进行性能分析和检测。
  • 在主线程上避免执行耗时操作,尤其是 I/O 操作、网络请求等,可以将这些操作放到后台线程或使用异步方式执行。
  • 合理分段长时间运行的任务,避免长时间的单次执行。
  • 在 UI 操作方面,尽量减少不必要的 UI 更新操作,使用合适的方式优化界面性能。
  • 使用 GCD 或 Operation Queue 等多线程技术,合理管理并发任务,避免资源竞争和死锁。
  • 在主线程中优先处理用户交互事件和 UI 更新,避免被其他低优先级任务占用。

2 卡顿检测

主流方案:主线程卡顿监控。通过开辟一个子线程来监控主线程的 RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。

实现思路:开辟一个子线程,然后实时计算 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况,如果主线程发生卡顿,这时我们要保存应用的上下文,即卡顿发生时程序的堆栈调用和运行日志上传。

2.1 NSRunLoop 实现

objectivec 复制代码
#import <Foundation/Foundation.h>

@interface LagMonitor : NSObject

+ (instancetype)sharedInstance;
- (void)startMonitoring;

@end

@implementation LagMonitor {
    id _observer;
    dispatch_semaphore_t _semaphore;
    BOOL _isMonitoring;
}

+ (instancetype)sharedInstance {
    static LagMonitor *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[LagMonitor alloc] init];
    });
    return instance;
}

- (void)startMonitoring {
    if (_isMonitoring) {
        return;
    }
    
    _isMonitoring = YES;
    
    // 创建信号量,用于控制RunLoop监测的时间间隔
    _semaphore = dispatch_semaphore_create(0);
    
    // 创建观察者,监听RunLoop的各个阶段
    _observer = [NSRunLoopObserver
                 observerWithActivity:NSRunLoopAllActivities
                 repeats:YES
                 callback:^(NSRunLoopObserver *observer, NSRunLoopActivity activity) {
                     [self runLoopObserverCallback];
                 }];

    if (_observer) {
        // 将观察者添加到主线程的RunLoop中
        [[NSRunLoop mainRunLoop] addObserver:_observer];
    
        // 创建一个子线程用于监测RunLoop的状态
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (_isMonitoring) {                // 等待信号量,即等待指定的时间间隔
                long semaphoreWait = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
                if (semaphoreWait != 0) {
                    // 如果信号量等待超时,则认为主线程出现卡顿
                    [BacktraceLogger printMainThreadStack];
                }
            }
        });
    } else {
        NSLog(@"创建 NSRunLoopObserver 失败");
    }
}

- (void)runLoopObserverCallback {
    // 发送信号量,通知子线程主线程的RunLoop正在运行
    dispatch_semaphore_signal(_semaphore);
}

@end

2.2 CFRunLoopRef 实现

objectivec 复制代码
#import <Foundation/Foundation.h>

@interface LagMonitor : NSObject

+ (instancetype)sharedInstance;
- (void)startMonitoring;

@end

@implementation LagMonitor {
    CFRunLoopObserverRef _observer;
    dispatch_semaphore_t _semaphore;
    BOOL _isMonitoring;
}

+ (instancetype)sharedInstance {
    static LagMonitor *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[LagMonitor alloc] init];
    });
    return instance;
}

- (void)startMonitoring {
    if (_isMonitoring) {
        return;
    }
    
    _isMonitoring = YES;
    
    // 创建信号量,用于控制RunLoop监测的时间间隔
    _semaphore = dispatch_semaphore_create(0);
    
    // 创建观察者,监听RunLoop的各个阶段
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallback, &context);
    if (_observer) {
        // 将观察者添加到主线程的RunLoop中
        CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
        
        // 创建一个子线程用于监测RunLoop的状态
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (_isMonitoring) {                // 等待信号量,即等待指定的时间间隔
                long semaphoreWait = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
                if (semaphoreWait != 0) {
                    // 如果信号量等待超时,则认为主线程出现卡顿
                    [BacktraceLogger printMainThreadStack];
                }
            }
        });
    } else {
        NSLog(@"创建 CFRunLoopObserverRef 失败");    
    }
}

void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    LagMonitor *monitor = (__bridge LagMonitor *)info;
    // 发送信号量,通知子线程主线程的RunLoop正在运行
    dispatch_semaphore_signal(monitor->_semaphore);
}

@end

2.3 打印信息

swift 复制代码
import Foundation

class BacktraceLogger {

    // 在需要时打印主线程的堆栈信息
    static func printMainThreadStack() {
        if Thread.isMainThread {
            if let callStackSymbols = Thread.callStackSymbols as? [String] {
                print("Main Thread Stack Trace:")
                for symbol in callStackSymbols {
                    print(symbol)
                }
            }
        } else {
            DispatchQueue.main.async {
                printMainThreadStack()
            }
        }
    }

    // 在程序启动时开始监控崩溃
    static func startMonitoringCrashes() {
        NSSetUncaughtExceptionHandler { exception in
            print("Crash Detected:")
            print(exception)
            print(exception.callStackSymbols.joined(separator: "\n"))
        }
    }
}

2.4 使用示例

objectivec 复制代码
// 开始监测主线程的卡顿情况
[[LagMonitor sharedInstance] startMonitoring];
        
// 模拟主线程执行任务,可以在这里进行一些耗时操作
// 这里只是一个简单的示例,实际应用中需要根据具体情况进行调整
for (NSInteger i = 0; i < 1000000000; i++) {
    NSLog(@"执行任务 %ld", (long)i);
}

2.5 具体方案

ini 复制代码
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    
    // 记录状态值
    object->activity = activity;
    
    // 发送信号
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 创建信号
    semaphore = dispatch_semaphore_create(0);
    
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    // 检测到卡顿,进行卡顿上报
                }
            }
            timeoutCount = 0;
        }
    });
}
相关推荐
m0_641031059 小时前
在选择iOS代签服务前,你必须了解的三大安全风险
ios
开开心心loky10 小时前
[iOS] push 和 present Controller 的区别
ui·ios·objective-c·cocoa
白玉cfc15 小时前
【iOS】push,pop和present,dismiss
macos·ios·cocoa
低调小一16 小时前
iOS 开发入门指南-HelloWorld
ios
2501_9159184117 小时前
iOS 开发全流程实战 基于 uni-app 的 iOS 应用开发、打包、测试与上架流程详解
android·ios·小程序·https·uni-app·iphone·webview
用户870568130451 天前
iOS 异步渲染:从 CALayer 切入的实现与优化
ios
敲代码的鱼哇2 天前
跳转原生系统设置插件 支持安卓/iOS/鸿蒙UTS组件
android·ios·harmonyos
在下历飞雨2 天前
Kuikly基础之状态管理与数据绑定:让“孤寡”计数器动起来
ios·harmonyos
在下历飞雨2 天前
Kuikly基础之Kuikly DSL基础组件实战:构建青蛙主界面
ios·harmonyos
鹏多多.2 天前
flutter-使用fluttertoast制作丰富的高颜值toast
android·前端·flutter·ios