监控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);
}
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
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"))
}
}
}
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;
}
});
}
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
});
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,可以判定为频次较快的一次有效卡顿
实践: 我们需要开启一个子线程,实时计算两个状态区域之间的耗时是否到达某个阀值。另外卡顿需要覆盖到多次连续小卡顿和单次长时间卡顿两种情景。