用户层面感知的卡顿都是来自处理所有UI的主线程上,包括在主线程上进行的大计算,大量的IO操作,或者比较重的绘制工作。
- 当监控到应用出现卡顿,如何定位造成卡顿的原因呢?很明显如果我们能够在发生卡顿的时候,保存应用的上下文,即卡顿发生时程序的堆栈调用和运行日志,那么就能凭借这些信息更加高效的定位到造成卡顿问题的来源。
寻找卡顿的切入点
如何监控主线程呢,首先需要知道的是主线程和其它线程一样都是靠NSRunLoop来驱动的。可以先看看CFRunLoopRun的大概的逻辑
int32_t __CFRunLoopRun()
{
// 通知即将进入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知将要处理timer和source
// kCFRunLoopBeforeSources 即将处理source
// kCFRunLoopAfterWaiting 刚从睡眠中唤醒
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 处理非延迟的主线程调用
__CFRunLoopDoBlocks();
// 处理UIEvent事件
__CFRunLoopDoSource0();
// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();
// 即将进入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待内核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// Zzz...
// 从等待中醒来
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 处理因timer的唤醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();
// 处理异步方法唤醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// UI刷新,动画显示
else
__CFRunLoopDoSource1();
// 再次确保是否有同步的方法需要调用
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即将退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}
不难发现NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿.
量化卡顿的程度
要监控NSRunLoop的状态,我们需要使用到CFRunLoopObserverRef,通过它可以实时获得这些状态值的变化,具体的使用如下:
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MyClass *object = (__bridge MyClass*)info;
object->activity = activity;
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
只需要另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手.
用GCD里的dispatch_semaphore_t开启一个新线程,设置一个极限值和出现次数的值,然后获取主线程上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting两个状态之间的超过了极限值和出现次数的场景,将堆栈dump下来,最后发到服务器做收集,通过堆栈能够找到对应出问题的那个方法。
- (void)start
{
if (observer)
return;
// // 创建信号
semaphore = dispatch_semaphore_create(0);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子线程监控时长
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));
// Returns zero on success, or non-zero if the timeout occurred.
if (st != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
// kCFRunLoopBeforeSources 即将处理source kCFRunLoopAfterWaiting 刚从睡眠中唤醒
// RunLoop会一直循环检测,从线程start到线程end,检测检测到事件源(CFRunLoopSourceRef)执行处理函数,首先会产生通知,corefunction向线程添加runloopObservers来监听事件,并控制NSRunLoop里面线程的执行和休眠,在有事情做的时候使当前NSRunLoop控制的线程工作,没有事情做让当前NSRunLoop的控制的线程休眠。
if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 3)
continue;
NSLog(@"有点儿卡");
}
}
timeoutCount = 0;
}
});
}