在開發中,我們可以使用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的狀態:
使用信號量dispatch_semaphore來控制對RunLoop狀態判斷的節奏,這個可以保證,每個RunLoop狀態的判斷都會進行。
對RunLoop狀態的判斷,我們專門在另外一個線程做判斷。
需要注意的是,對卡頓的判斷是通過
kCFRunLoopBeforeSources
或者kCFRunLoopBeforeWaiting
這兩個狀態開始后,信號量+1,這時候信號量>0,dispatch_semaphore_wait
不會阻塞,返回0,進行下一個while循環,如果此時還沒有進入下一個RunLoop狀態,此時信號量=0,dispatch_semaphore_wait
就會在這里阻塞,到了設定的超時時間,dispatch_semaphore_wait
的返回值>0,這時候就會進行耗時的判斷。
我們可以自己設定超時時間和超過多少次算卡頓,這里設置超過250ms。