性能优化02.2:Runloop监测

张建 lol

RunLoop 原理

  1. RunLoopiOS 里由 CFRunLoop 实现。简单来说,RunLoop 是用来监听输入源,进行调度处理的。
  • 这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调。

  • RunLoop 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件。

  • RunLoop 的目的是,当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠。所以,RunLoop 不光能够运用到监控卡顿上,还可以提高用户的交互体验。通过将那些繁重而不紧急会大量占用 CPU 的任务(比如图片加载),放到空闲的 RunLoop 模式里执行,就可以避开在 UITrackingRunLoopMode 这个 RunLoop 模式时执行

  1. RunLoop 执行流程
  • 在RunLoop运行的整个过程中,loop 的状态包括 6 个:
1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 进入 loop
kCFRunLoopBeforeTimers , // 触发 Timer 回调
kCFRunLoopBeforeSources , // 触发 Source0 回调
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有状态改变
}

Runloop 流程图:

注:
1、Source0 被添加到 RunLoop 上时并不会主动唤醒线程,需要手动去唤醒。Source0 负责对触摸事件的处理以及 performSeletor:onThread:
2、Source1 具备唤醒线程的能力,使用的是基于 Port线程间通信Source1 负责捕获系统事件,并将事件交由 Source0 处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RunLoop 顺序:
1、进入

2、通知Timer
3、通知Source
4、处理Block
5、处理Source0
6、如果有 Source1 调转到 11
7、通知 BeforWaiting
8、休眠 wait:等待线程被唤醒
9、通知 afterWaiting
10、处理唤醒线程消息: timer、GCD相关、处理Source1
11、处理 dispatch 到 main_queue 的 block
12、根据情况,决定如何操作:回到 2、退出Runloop

13、退出 Runloop
  • 理清楚Runloop的 运行机制,就很容易明白处理事件主要有两个时间段 kCFRunLoopBeforeSources 发送之后和 kCFRunLoopAfterWaiting 发送之后。

  • dispatch_semaphore_t 是一个信号量机制,信号量到达、或者超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。

  • 利用这个特性我们判断卡顿出现的条件为 在信号量发送 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 后进行了大量的操作,在一段时间内没有再发送信号量,导致超时。也就是说 主线程通知 状态 长时间的停留在这两个状态上了。转换为代码就是判断有没有超时, 超时 了,判断当前停留的状态是不是这两个状态,如果是,就判定为 卡顿

  1. 这样就能解释通为什么要用这两个信号量判断卡顿。这么一个简单的问题,思路转不过来就绕进去了,现在回看感觉这个很简单,也是耗了一天时间。

  2. 要利用 RunLoop 原理来监控卡顿的话,要关注两个阶段。分别是 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting ,就是要触发 Source0 回调和接收 mach_port 消息两个状态。

具体实现

  1. 创建一个 ZJRunloopMonitor 类

.h 文件

1
2
3
4
5
6
7
8
9

@interface ZJRunloopMonitor : NSObject
// 单例
+ (instancetype)shareInstance;
// 开始监测
- (void)startMonitor;
// 停止监测
- (void)stopMonitor;
@end

.m 文件

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#import "ZJRunloopMonitor.h"

@interface ZJRunloopMonitor (){
// 信号量
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopObserverRef runLoopObserver;
CFRunLoopActivity runLoopActivity;
NSInteger timeoutCount;
}
@end
@implementation ZJRunloopMonitor
// 单例
+ (instancetype)shareInstance{
static ZJRunloopMonitor * instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[ZJRunloopMonitor alloc] init];
});
return instance;
}

#pragma mark -开始监测
- (void)startMonitor{
// 如果有监测,则返回
if (runLoopObserver) {return;}

// 创建 CFRunLoopObserverContext 观察者
/**
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;
*/

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL,NULL};
/**
CFRunLoopObserverRef CFRunLoopObserverCreate(
CFAllocatorRef allocator,
CFOptionFlags activities,
Boolean repeats,
CFIndex order,
CFRunLoopObserverCallBack callout,
CFRunLoopObserverContext *context
);
*/
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
// 添加观察者
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

// 向主线程添加 观察者
CFRunLoopRef mainLoop = CFRunLoopGetMain();
CFRunLoopAddObserver(mainLoop, runLoopObserver, kCFRunLoopCommonModes);

// 创建子线程开始监控
dispatch_queue_t monitorQueue = dispatch_queue_create("com.zj.monitorQueue", DISPATCH_QUEUE_CONCURRENT);

// 创建同步信号量
dispatchSemaphore = dispatch_semaphore_create(0);

//创建子线程开始监控
dispatch_async(monitorQueue, ^{
// 子线程开启一个持续的loop用来进行监控
while (YES) {
// 超时时间设置 2s
dispatch_time_t outTimer = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);

// 信号量到达、或者 超时会继续向下进行,否则等待
long result = dispatch_semaphore_wait(self->dispatchSemaphore, outTimer);

if (result != 0) {
// 超时,判断最后停留的信号量是哪一个,是否处理为卡顿现象。
if (!self->runLoopObserver) {
NSLog(@"--NO runLoopObserver---");
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
//判断当前 监听到的 信号(也就是说上一个信号量超过2秒没有更新,故卡顿)
if (self->runLoopActivity == kCFRunLoopBeforeSources ||
self->runLoopActivity == kCFRunLoopAfterWaiting) {
// 出现卡顿、进一步处理
NSLog(@"--卡顿啦----From 卡顿监控线程");
// log current stack info
continue;
}
}
NSLog(@"--系统运行良好--From 卡顿监控线程");
}
});

}

#pragma mark - 停止监测
- (void)stopMonitor{
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
CFRelease(runLoopObserver);
runLoopObserver = NULL;
}

#pragma mark -监控是否处于 运行状态
// 观察者回调函数
void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
// 每一次监测到Runloop发送通知的时候,都会调用此函数
// 在此过程修改当前的 RunloopActivity 状态,发送同步信号。
ZJRunloopMonitor * monitor = (__bridge ZJRunloopMonitor *)info;
monitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = monitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}

@end
  1. 观察RunLoop 的 common 模式
  • 将创建好的观察者 runLoopObserver 添加到主线程 RunLoopcommon 模式下观察。
    然后,创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。

  • 一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为 卡顿

  • 代码中触发卡顿的时间阈值 ,设置成了 2 秒。这个 2 秒的阈值合理?我们可以根据 WatchDog 机制来设置。WatchDog 在不同状态下设置的不同时间,如下所示:

启动(Launch):20s;
恢复(Resume):10s;
挂起(Suspend):10s;
退出(Quit):6s;
后台(Background):3min(在 iOS 7 之前,每次申请 10min; 之后改为每次申请 3min,可连续申请,最多申请到 10min)。

  • 接下来,我们就可以 log 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

如何获取卡顿的方法堆栈信息

  • 子线程监控发现卡顿后,还需要记录当前出现卡顿的方法堆栈信息,并适时推送到服务端供开发者分析,从而解决卡顿问题。

  • 直接调用系统函数获取堆栈

  • 这种方法的优点在于,性能消耗小。但是,它只能够获取简单的信息,也没有办法配合 dSYM 来获取具体是哪行代码出了问题,而且能够获取的信息类型也有限。

  • 但因为性能比较好,所以适用于观察大盘统计卡顿情况,而不是想要找到卡顿原因的场景。

  1. 直接获取堆栈信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <libkern/OSAtomic.h>
#include <execinfo.h>

//获取函数堆栈信息
+ (NSArray *)backtrace {
void* callstack[128];
int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (i = 0;
i < backtrace.count;
i++) {
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
  1. 三方库

直接用 PLCrashReporter 这个开源的第三方库来获取堆栈信息。这种方法的特点是,能够定位到问题代码的具体位置,而且性能消耗也不大。

1
2
3
4
5
6
7
8
// 获取数据
NSData *lagData = [[[PLCrashReporter alloc] initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
// 转换成 PLCrashReport 对象
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
// 进行字符串格式化处理
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
//将字符串上传服务器
NSLog(@"lag happen, detail below: \n %@",lagReportString);
  • Post title:性能优化02.2:Runloop监测
  • Post author:张建
  • Create time:2023-03-23 08:07:53
  • Post link:https://redefine.ohevan.com/2023/03/23/OC性能优化/性能优化02.2:Runloop监测/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
On this page
性能优化02.2:Runloop监测