iOS之卡顿检测方案

卡顿检测的方案根据线程是否相关分为两大类:

  • 检测任务耗时
  • 检测主线程是否能响应任务

与主线程相关的检测方案:

  • fps
  • ping
  • RunLoop

与主线程不相关的检测方案:

  • stack backtrace
  • msgSend Observe

FPS监测

原理:

  1. 通常情况下,屏幕会保持60hz/s的刷新速度。
  2. 每次刷新时会发出一个屏幕刷新信号,CADisplayLink允许我们注册一个与刷新信号同步的回调处理。
  3. 记录上次计算fps的时间,计算本次回调的时间和上次计算时间间隔interval,计算fps:fps = 1 / interval
  4. 每次计算fps的同时,更新上次计算fps时间为当前时间,开始新一轮的记录。

获取fps:

1
2
3
4
5
6
7
8
9
10
11
- (void)startFpsMonitoring {
WeakProxy *proxy = [WeakProxy proxyWithClient: self];
self.fpsDisplay = [CADisplayLink displayLinkWithTarget: proxy selector: @selector(tick:)];
[self.fpsDisplay addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes];
}
- (void)tick:(CADisplayLink *)link
{
NSTimeInterval deltaTime = link.timestamp - self.lastTime;
self.currentFPS = 1 / deltaTime;
self.lastTime = link.timestamp;
}

Ping

ping是常用的网络测试工具,用来测试数据包能否到达ip地址。

大多数手机的屏幕刷新频率是60HZ,如果在 1000/60=16.67ms 内没有将这一帧的任务执行完毕,就会发生丢帧现象,这便是用户感受到卡顿的原因。

实现思路:创建一个子线程进行循环检测,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程休眠设定的阈值,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。

主要代码:

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
self.semaphore = dispatch_semaphore_create(0);
- (void)main {
//判断是否需要上报
__weak typeof(self) weakSelf = self;
void (^ verifyReport)(void) = ^() {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf.reportInfo.length > 0) {
if (strongSelf.handler) {
double responseTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
double duration = responseTimeValue - strongSelf.startTimeValue;
if (DEBUG) {
NSLog(@"卡了%f,堆栈为--%@", duration, strongSelf.reportInfo);
}
strongSelf.handler(@{
@"title": [InsectUtil dateFormatNow].length > 0 ? [InsectUtil dateFormatNow] : @"",
@"duration": [NSString stringWithFormat:@"%.2f",duration],
@"content": strongSelf.reportInfo
});
}
strongSelf.reportInfo = @"";
}
};

while (!self.cancelled) {
if (_isApplicationInActive) {
self.mainThreadBlock = YES;
self.reportInfo = @"";
self.startTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
dispatch_async(dispatch_get_main_queue(), ^{
self.mainThreadBlock = NO;
dispatch_semaphore_signal(self.semaphore);
});
[NSThread sleepForTimeInterval:(self.threshold/1000)];
if (self.isMainThreadBlock) {
self.reportInfo = [InsectBacktraceLogger insect_backtraceOfMainThread];
}
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
//卡顿超时情况;
verifyReport();
} else {
[NSThread sleepForTimeInterval:(self.threshold/1000)];
}
}
}

美团Hertz实现思路:

检测主线程每次执行消息循环的时间,当这一时间大于阈值时,就记为发生一次卡顿。

runloop

RunLoop在BeforeSourcesAfterWaiting后会进行任务的处理。可以在此时阻塞监控线程并设置超时时间,若超时后RunLoop的状态仍为RunLoop在BeforeSourcesAfterWaiting,表明此时RunLoop仍然在处理任务,主线程发生了卡顿。

  1. 我们需要创建一个 CFRunLoopObserverContext 观察者,且创建一个 Observer,并监控主线程状态的变化
  2. 这个Observer会监听 kCFRunLoopAllActivities(所有状态改变),并在状态改变时执行 runLoopObserverCallBack 中的代码。
  3. 创建一个子线程,使用while循环保活,并通过信号量阻塞该线程
  4. 如果各状态切换没有发生阻塞,那么会及时发出信号量的激活信号,代码继续执行,不视为卡顿。反之各状态耗时过长,没有及时发出信号,则视为发生卡顿。
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
64
- (void)beginMonitor {
self.dispatchSemaphore = dispatch_semaphore_create(0);
// 第一个监控,监控是否处于 运行状态
CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
LONG_MIN,
&myRunLoopBeginCallback,
&context);
// 第二个监控,监控是否处于 睡眠状态
self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
LONG_MAX,
&myRunLoopEndCallback,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);

// 创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {
self.timeoutCount = 0;
self.dispatchSemaphore = 0;
self.runLoopBeginActivity = 0;
self.runLoopEndActivity = 0;
return;
}
// 两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||
(self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {

if (++self.timeoutCount < 3) {
continue;
}
// 每监控到三次卡顿,就上报
NSLog(@"调试:监测到卡顿");
}
}
self.timeoutCount = 0;
}
});
}

// 第一个监控,监控是否处于 运行状态
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
lagMonitor.runLoopBeginActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphore); // 发出信号
}

// 第二个监控,监控是否处于 休眠状态
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
lagMonitor.runLoopEndActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}

stack backtrace

代码质量不够好的方法可能会在一段时间内持续占用CPU的资源。

如果在一段时间内,调用栈总是停留在执行某个地址指令的状态。由于函数调用会发生入栈行为,比对两次调用栈的符号信息,如果前者是后者的符号子集时,可以认为出现了卡顿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface StackBacktrace : NSThread
......
@end

@implementation StackBacktrace

- (void)main {
[self backtraceStack];
}

- (void)backtraceStack {
while (!self.cancelled) {
@autoreleasepool {
NSSet *curSymbols = [NSSet setWithArray: [StackBacktrace backtraceMainThread]];
if ([_saveSymbols isSubsetOfSet: curSymbols]) {
......
}
_saveSymbols = curSymbols;
[NSThread sleepForTimeInterval: _interval];
}
}
}

@end

msgSend observe

OC方法的调用最终转换成objc_msgSend的调用执行,通过在函数前后插入自定义的函数调用,维护一个函数栈结构可以获取每一个OC 方法的调用耗时。这种方式比较适合在开发阶段分析、优化代码;另外就是局限于OC语言:

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
#define save() \\
__asm volatile ( \\
"stp x8, x9, [sp, #-16]!\\n" \\
"stp x6, x7, [sp, #-16]!\\n" \\
"stp x4, x5, [sp, #-16]!\\n" \\
"stp x2, x3, [sp, #-16]!\\n" \\
"stp x0, x1, [sp, #-16]!\\n");

#define resume() \\
__asm volatile ( \\
"ldp x0, x1, [sp], #16\\n" \\
"ldp x2, x3, [sp], #16\\n" \\
"ldp x4, x5, [sp], #16\\n" \\
"ldp x6, x7, [sp], #16\\n" \\
"ldp x8, x9, [sp], #16\\n" );

#define call(b, value) \\
__asm volatile ("stp x8, x9, [sp, #-16]!\\n"); \\
__asm volatile ("mov x12, %0\\n" :: "r"(value)); \\
__asm volatile ("ldp x8, x9, [sp], #16\\n"); \\
__asm volatile (#b " x12\\n");

__attribute__((__naked__)) static void hook_Objc_msgSend() {

save()
__asm volatile ("mov x2, lr\\n");
__asm volatile ("mov x3, x4\\n");

call(blr, &push_msgSend)
resume()
call(blr, orig_objc_msgSend)

save()
call(blr, &pop_msgSend)

__asm volatile ("mov lr, x0\\n");
resume()
__asm volatile ("ret\\n");
}