前言
runloop不只是停留在面试的认知上,实际在开发中也可以利用其来处理一些特出情况,例如:通过runloop检测主线程卡顿情况,通过runloop加载较大任务等
本文主要介绍使用runloop检测主线程卡顿情况,并打印出卡顿代码的调用栈信息
runloop简介
之前介绍到了runloop有多个mode,也就是有多个不同的运行模式,一次只能在一个模式下运行,且通过mode切换来达到切换状态的效果,其mode,如下所示
NSDefaultRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSEventTrackingRunLoopMode
NSRunLoopCommonModes
其中最后一个NSRunLoopCommonModes实际上是不属于基本运行mode,他是所有mode的集合,即设置了NSRunLoopCommonModes参数的代码,可以在各个mode模式下正常执行
如果想尽可能减少用户操作时的事件,可以将任务放到NSDefaultRunLoopMode模式下运行
此外在温习一下runloop运行的流程图,可以清楚的看到observer的调用调用
然后查看一下runloop代码枚举给出的可以监听的observer类型
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变
};
复制代码
因此当runloop长时间停在kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting状态,可以认为runloop卡在了任务执行之前,为了避免减少误差,一定次数可以认为是一次有效卡顿,这也是我们小工具的核心逻辑
runloop卡顿检测工具
前面介绍了我们的卡顿是通过观测runloop是否长时间停在kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting状态来检测卡顿,下面介绍下实现逻辑
创建observer监听主线程状态
设置runloopObserver来监听主队列runloop的状态变化,并设置回调方法,最后添加到commonMode上,以保证所有模式都能监听到状态变化
//注册observer监听runloop
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &runloopCallback, &context);
//添加observer到主队列的commonMode上
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
复制代码
设置runloop的observer改变后的回调方法
void runloopCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunloopMonitor *monitor = [RunloopMonitor sharedInstance];
//使用工具类保存主线程的状态activity
monitor->_activity = activity;
//释放信号量(信号值+1,如果信号量值低于0则阻塞)
dispatch_semaphore_signal(monitor->_semaphore);
}
复制代码
子线程监听主线程状态activity
在子线程中,配合信号量实现卡顿检测
开始检测功能之前,先开启信号量机制,设置默认信号量为0,当信号量值小于0时,会阻塞当前队列,使用wait方法会使信号量-1,使用signal会使信号量+1
子队列开启循环,每次通过wait方法阻塞子队列,并设置超时时间,一旦超过超时时间则会自动解除阻塞继续执行代码,当子队列收到主队列收到的消息后也会解除阻塞,实现正常功能
可以看到,正常没有卡顿情况下,主队列会在子队列超时之前切换runloopMode从而signal来释放信号,进而解除子线程阻塞,因此wait方法会返回一个为0的参数,表示没有卡顿;
当主线程长时间不调用signal,子线程会等待超时,因此会通过wait方法返回一个不为0的result,来继续执行代码,此时可以认为是主线程存在卡顿可能,因此查看主线程状态是否是kCFRunLoopBeforeSources、kCFRunLoopAfterWaiting,如果是可以认为主线程卡在了此方法中(为了操作巧合,使用了一定次数来矫正为一次卡顿),如果不是处于此状态,表示没有卡在处理方法哪里,可以认为不卡顿
检测到一定次数的卡顿后,认为一次有效卡顿,则可以回调对应的方法,或者打印调用栈信息等
代码如下所示:
//初始化信号量等相关参数
_semaphore = dispatch_semaphore_create(0);
_semaphoreCount = 0;
_cardCount = 0;
__weak typeof(self) wself = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__strong typeof(self) sself = wself;
if (!sself) return;
while (sself->_isMonitor) {
//使用信号量阻塞当前线程(信号量值-1,如果信号量值低于0则阻塞),设置一个超时时间400ms
//如果超时时间到了,则会停止阻塞,信号量恢复,并返回一个非零的resut
//如果是正常signal唤醒,则result返回0
intptr_t result = dispatch_semaphore_wait(sself->_semaphore,
dispatch_time(DISPATCH_TIME_NOW, sself->_minInterval * NSEC_PER_MSEC));
//如果等待超时,runloop仍然在等到sources处理或者刚刚唤醒状态,被认为一次卡顿
if (result != 0 && (sself->_activity == kCFRunLoopBeforeSources
|| sself->_activity == kCFRunLoopAfterWaiting)) {
if (++sself->_cardCount >= sself->_maxCount) {
//大于或者等于为一次有效卡顿,回调卡顿提示block
if (sself.runloopMonitorCardCallback) sself.runloopMonitorCardCallback(sself);
if (sself.isPrintStackSymbols) __printStackSymbols(sself);
sself->_cardCount = 0;
}
}else {
//没有超时,则重置卡顿此时
sself->_cardCount = 0;
}
};
});
复制代码
此外,还加入了调用栈信息打印的方法,且去除了重复的调用栈打印信息(具体的打印方法是以前看来的,忘了哪里看的),注意仅仅支持在debug下打印,且xcode有时打印会有问题,可以重新尝试
代码如下所示
//打印堆栈信息
void __printStackSymbols(RunloopMonitor *self) {
NSString *callStackSymbols = [LSCallStack ls_backtraceOfMainThread];
//仅仅显示2s之外的重复卡顿信息,为了方便调试
if (!self->_lastCallStackSymbols ||
![self->_lastCallStackSymbols isEqualToString:callStackSymbols] ||
(self->_lastInterval && CACurrentMediaTime() - self->_lastInterval > 2) ) {
NSLog(@"检测到了卡顿\n 堆栈信息---callStackSymbols:\n%@\n", callStackSymbols);
}
self->_lastCallStackSymbols = callStackSymbols;
self->_lastInterval = CACurrentMediaTime();
}
复制代码
测试效果
接下来我们开启检测功能
[[RunloopMonitor sharedInstance] startMonitor];
复制代码
加入测试案例:
在tableView中sleep,设置了一个tap事件,方便后面单次点击测试
- (void)initTableView {
UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.frame
style:UITableViewStylePlain];
tableView.dataSource = self;
tableView.delegate = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"identifier"];
[self.view addSubview:tableView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(onTapTableView)];
[tableView addGestureRecognizer:tap];
}
- (void)onTapTableView {
[NSThread sleepForTimeInterval:3];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1000;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath];
cell.textLabel.text = @"我就测试一下";
if (indexPath.row % 10 == 0) {
[NSThread sleepForTimeInterval:1];
}
return cell;
}
复制代码
打印效果图如下所示,可以看到效果非常nice
最后
可以参考着源码查看理解,这里代码粘了一部分,这边是runloop的应用之一了,下一章介绍通过runloop加载任务(最多的是大图)