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;
        }
    });
}
相关推荐
Jouzzy7 小时前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克7 小时前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨8 小时前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆9 小时前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂1 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T1 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20251 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz1 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频
安和昂2 天前
【iOS】SDWebImage源码学习
学习·ios
ii_best2 天前
按键精灵ios脚本新增元素功能助力辅助工具开发(三)
ios