Runloop - 卡顿检测
2023-06-26 本文已影响0人
xxttw
卡顿主要表现为主线程卡死, 不响应用户操作或响应很慢, 这种体验很差, 会让用户对产品的好感地急速下滑, 如果不及时优化, 最终会导致用户流失
- 哪些情况会导致主线程卡顿呢? 大致有如下几个方面
- 复杂的UI布局, 大量的图文混排绘制
- 复杂耗时的计算逻辑
- 主线程大量的IO操作
- 主线程同步发起网络请求
- 主线程死锁
检测方案
为了优化卡顿, 我们需要准确的知道哪里发生了卡顿, 然后才能针对性的进行优化
检测FPS幅度其实是一种方案, 但是并不一定准确, 因为像动画片或者其他.24FPS就可以达到流程播放的程度.
另一方方案是Runloop, 为什么Runloop可以做到卡顿监控呢, 因为我们的程序中任务都是在线程中执行, 线程又依赖于Runloop, 并且Runloop总是在相应的状态执行任务, 执行完成后就会切换到下一个状态, 如果Runloop在一个状态下执行任务的时间过长,无法进行下一个状态, 我们就可以认为发生了卡顿, 我们要做的就做检测Runloop切换状态的耗时, 多长时间可以自己控制,比方说0.5秒
Runloop的运行状态如下
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 进入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 处理Timer事件
kCFRunLoopBeforeSources = (1UL << 2), // 处理Source事件
kCFRunLoopBeforeWaiting = (1UL << 5), // 进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 唤醒
kCFRunLoopExit = (1UL << 7), // 退出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};
执行流程如下
image.png在一次循环中, Timer事件, Source事件, 唤醒后的事件处理时间过长都可以认为是发生卡顿, 还有休眠之前的事件, 休眠的时间不算卡顿
具体思路实现
- 我们创建一个子线程,在子线程的里监听主线程Runloop的各种状态切换
self.ob = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0, runLoopObserverCallBack, NULL);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.ob, kCFRunLoopCommonModes);
- 在回调函数中, 记录当前模式, 并将信号量发送出去, 以便后续进行检测任务的处理
void runLoopObserverCallBack(CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info)
{
[FluecyMonitor shared].currentActivity = activity;
dispatch_semaphore_t sema = [FluecyMonitor shared].semaphore;
dispatch_semaphore_signal(sema);
}
- 在子线程中开启循环检测
- 如果当前是kCFRunLoopBeforeWaiting状态, 我们给主线程添加一个任务(timeout = NO), 子线程休眠一段时间后检测 任务是否完成, 如果没有完成说明改状态下存在卡顿情况.
- 如果当前是其他状态, 信号量等待一定时间 任然无法获得线程资源, dispatch_semaphore_wait返回值为非0 则说明主线程runloop回调没有执行, 则也可能是发生了卡顿
- (void)startMonitor{
self.ob = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0, runLoopObserverCallBack, NULL);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.ob, kCFRunLoopCommonModes);
self.isMonitoring = YES;
self.semaphore = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 在子线程中监控
dispatch_async(queue, ^{
while (self.isMonitoring) {
if (self.currentActivity == kCFRunLoopBeforeWaiting) {
// 处理休眠前的事件
__block BOOL timeout = YES;
// 在主线程中添加已给任务
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程可以执行的话 就是告诉你他没有卡顿
timeout = NO;
});
//子线程休眠0.2秒 如果timeout还是YES 说明添加到主线程的任务没有被执行
[NSThread sleepForTimeInterval:TimeOutIntevl];
if (timeout) {
[LXDBacktraceLogger lxd_logMain];
}
} else {
// 处理timer, source,唤醒后的事件
// 在执行时间内0.02 * NSEC_PER_SEC 无法获取到信号量资源.则会返回一个非0的值
long waitValue = dispatch_semaphore_wait(self.semaphore, 0.02 * NSEC_PER_SEC);
// waitValue != 0 相当于是信号量等待超时了.说明主线程runloop回调一直没有发送出来
if (waitValue != 0) { // 如果是超时 还一直卡在这几个状态下, 那肯定是主线程卡了
if (self.currentActivity == kCFRunLoopBeforeSources ||
self.currentActivity == kCFRunLoopBeforeTimers ||
self.currentActivity == kCFRunLoopAfterWaiting) {
[LXDBacktraceLogger lxd_logMain];
}
}
}
}
});
}
- 当Runloop 状态的切换超过了设置的阈值时, 我们就打印打钱主线程调用方法的堆栈信息,来准确的定位到底是那一个方法造成了卡顿,进而针对性的进行优化
- (void)tableView: (UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath {
sleep(2.0);
}
2022-12-31 00:22:58.343287+0800 RunLoop卡顿监控[13273:946345] 主线程卡顿 Backtrace of Thread 259:
======================================================================================
libsystem_kernel.dylib 0x1cc0594c4 __semwait_signal + 8
libsystem_c.dylib 0x1800ff774 nanosleep + 216
libsystem_c.dylib 0x1800ff570 sleep + 48
RunLoop卡顿监控 0x100a6becc -[ViewController tableView:didSelectRowAtIndexPath:] + 84
UIKitCore 0x184fb4854 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:deselectPrevious:] + 1620
UIKitCore 0x184fb41e8 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 112
UIKitCore 0x184fb4ad0 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 316
UIKitCore 0x185294438 -[_UIAfterCACommitBlock run] + 64
UIKitCore 0x18529490c -[_UIAfterCACommitQueue flush] + 188
UIKitCore 0x184d97a0c _runAfterC
- 我们在cell的点击事件里加入了休眠了2秒.造成了Runloop状态切换超过了阈值, 所以自动检测到了卡顿信息,我们可以根据信息修复卡顿问题