在开发中,我们可以使用Xcode自带的Instruments工具的Core Animation来对APP运行流畅度进行监控,使用FPS这个值来衡量。这个工具我们只能知道哪个界面会有卡顿,无法知道到底是什么操作哪个函数导致的卡顿。
界面出现卡顿,一般是下面几种原因:
主线程做大量计算
主线程大量的I/O操作
大量的UI绘制
主线程进行网络请求以及数据处理
离屏渲染
监控界面卡顿,主要是监控主线程做了哪些耗时的操作,iOS中线程的事件处理依靠的是RunLoop,正常FPS值为60,如果单次RunLoop运行循环的事件超过16ms,就会使得FPS值低于60,如果耗时更多,就会有明显的卡顿。
正常RunLoop运行循环一次的流程是这样的:
SetupThisRunLoopRunTimeOutTimer();
do {
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
__CFRunLoopDoSource0(); // 处理source0事件,UIEvent事件,比如触屏点击
CheckIfExitMessagesInMainDispatchQueue(); // 检查是否有分配到主队列中的任务
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
var wakeUpPort = SleepAndWaitForWakingUpPorts(); // 开始休眠,等待ma ch_msg事件
// mach_msg_trap
// ZZz..... sleep
// Received mach_msg, wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting); // 被事件唤醒
// Handle msgs
if (wakeUpPort == timePort) { // 被唤醒的事件是timer
__CFRunLoopDoTimers();
} else if (wakePort == mainDispatchQueuePort) { // 主队列有调度任务
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
} else { // source1事件,UI刷新,动画显示
__CFRunLoopDoSource1();
}
__CFRunLoopDoBlocks();
} while (!stop && !timeout)
从这个运行循环中可以看出,RunLoop
休眠的事件是无法衡量的,处理事件的部分主要是在kCFRunLoopBeforeSources
之后到kCFRunLoopBeforeWaiting
之前和kCFRunLoopAfterWaiting
之后和运行循环结束之前这两个部分
监控这两个部分的耗时,使用CFRunLoopObserverRef
来监控RunLoop
的状态:
首先创建observer
使用信号量dispatch_semaphore
来控制对RunLoop
状态判断的节奏,这个可以保证,每个RunLoop
状态的判断都会进行。
对RunLoop
状态的判断,我们专门在另外一个线程做判断。
__block NSInteger timeCount = 0;
_semaphore = dispatch_semaphore_create(0);
__weak typeof(self) weakSelf = self;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
weakSelf.myActivity = activity;
dispatch_semaphore_signal(weakSelf.semaphore);
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
然后开始检测卡顿情况:
需要注意的是,对卡顿的判断是通过kCFRunLoopBeforeSources
或者kCFRunLoopBeforeWaiting
这两个状态开始后,信号量+1,这时候信号量>0,dispatch_semaphore_wait
不会阻塞,返回0,进行下一个while循环,如果此时还没有进入下一个RunLoop
状态,此时信号量=0,dispatch_semaphore_wait
就会在这里阻塞,到了设定的超时时间,dispatch_semaphore_wait
的返回值>0,这时候就会进行耗时的判断。
我们可以自己设定超时时间和超过多少次算卡顿,这里设置超过250ms。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (true) {
long count = dispatch_semaphore_wait(weakSelf.semaphore, 50 * NSEC_PER_MSEC); //50毫秒
if (count != 0) {
if (!observer) {
weakSelf.semaphore = NULL;
timeCount = 0;
return ;
}
if (weakSelf.myActivity == kCFRunLoopBeforeSources) {
if (++timeCount < 5) { //连续5次就是250毫秒
continue;
} else {
NSLog(@"卡顿了");
}
}
}
timeCount = 0;
}
});
网友评论