洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

      • Runloop系列
      • CADisplayLink 与 NSTimer 有什么不同
      • 监控runloop卡顿
        • 什么原因导致了卡顿
        • 相关知识补充 - 信号量
        • 寻找卡顿切入点
    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • Runloop系列
洋仔
2024-09-03
目录

监控runloop卡顿

# 什么原因导致了卡顿

  • 死锁 (opens new window)
  • 抢锁
  • 大量的Ui绘制,复杂的UI,图文混排
  • 主线程大量IO、大量计算

# 相关知识补充 - 信号量

信号量就是一个资源计数器,对信号量有两个操作来达到互斥,分别是P和V操作。 一般情况是这样进行临界访问或互斥访问的: 设信号量值为1, 当一个进程1运行时,使用资源,进行P操作,即对信号量值减1,也就是资源数少了1个,这时信号量值为0。

系统中规定当信号量值为0时,必须等待,直到信号量值不为零才能继续操作。 这时如果进程2想要运行,那么也必须进行P操作,但是此时信号量为0,所以无法减1,即不能P操作,也就阻塞,这样就到到了进程1排他访问。

当进程1运行结束后,释放资源,进行V操作。资源数重新加1,这时信号量的值变为1. 这时进程2发现资源数不为0,信号量能进行P操作了,立即执行P操作。信号量值又变为0,这时进程2有资源,排他访问资源。 这就是信号量来控制互斥的原理。

# 寻找卡顿切入点

监控卡顿,最直接就是找到主线程都在干些啥玩意儿.我们知道一个线程的消息事件处理都是依赖于NSRunLoop来驱动,所以要知道线程正在调用什么方法,就需要从NSRunLoop来入手.CFRunLoop的代码是开源,可以在此处查阅到 CFRunLoop.c (opens new window) 源代码

其中核心方法CFRunLoopRun简化后的主要逻辑大概是这样的:

/// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {

        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

        /// 5. GCD处理main block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();


        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


    } while (...);

    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

不难发现NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

iOS如何监控线程卡顿?

说一下QiLagMonitor中的大致实现思路。

  • 首先,创建一个观察者runLoopObserver,用于观察主线程的runloop状态。同时,还要创建一个信号量dispatchSemaphore,用于保证同步操作。

  • 其次,将观察者runLoopObserver添加到主线程runloop中观察。

  • 然后,开启一个子线程,并且在子线程中开启一个持续的loop来监控主线程runloop的状态。

  • 如果发现主线程runloop的状态卡在为BeforeSources或者AfterWaiting超过88毫秒时,即表明主线程当前卡顿。这时候,我们保存主线程当前的调用堆栈即可达成监控目的。

#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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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"))
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

具体方案:

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;
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

卡顿监控的阈值计算

//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子线程开启一个持续的 loop 用来进行监控
    while (YES) {
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
        if (semaphoreWait != 0) {
            if (!runLoopObserver) {
                timeoutCount = 0;
                dispatchSemaphore = 0;
                runLoopActivity = 0;
                return;
            }
            //BeforeSources 和 AfterWaiting 这两个状态能够检测到是否卡顿
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                //将堆栈信息上报服务器的代码放到这里
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码中的NSEC_PER_SEC,代表的是触发卡顿的时间阈值,单位是秒。可以看到,我们把这个阈值设置成了3秒。那么,这个3秒的阈值是从何而来呢?这样设置合理吗?

其实,触发卡顿的时间阈值,我们可以根据WatchDog机制来设置。WatchDog在不同状态下设置为不同时间,如下所示:

  • 启动(Launch):20秒;
  • 恢复(Resume):10秒;
  • 挂起(Suspend):10秒;
  • 退出(Quit):6秒;
  • 后台(Background):3分钟(在iOS7之前,每次申请10分钟,之后改为每次申请3分钟,可以连续申请,最多申请到10分钟);

通过WatchDog设置的时间,我们可以把启动的阈值设置为10秒,其他状态则都默认为3秒。总的原则就是要小于WatchDog的限制时间。不过,这个阈值也不用小的太多,原则就是我们要优先解决用户感知最明显的体验问题;

# 量化卡顿的程度

原理: 利用观察Runloop各种状态变化的持续时间来检测计算是否发生卡顿 一次有效卡顿采用了“N次卡顿超过阈值T”的判定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:举例,卡顿阈值T=500ms、卡顿次数N=1,可以判定为单次耗时较长的一次有效卡顿;而卡顿阈值T=50ms、卡顿次数N=5,可以判定为频次较快的一次有效卡顿

实践: 我们需要开启一个子线程,实时计算两个状态区域之间的耗时是否到达某个阀值。另外卡顿需要覆盖到多次连续小卡顿和单次长时间卡顿两种情景。


1
编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
CADisplayLink 与 NSTimer 有什么不同
RunTime系列

← CADisplayLink 与 NSTimer 有什么不同 RunTime系列→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式