卡頓的原因:
- 復(fù)雜UI私股、圖文混排的繪制量過大;
- 在主線程做網(wǎng)絡(luò)同步請求冤留;
- 在主線程做大量的IO操作碧囊;
- 運算量過大,CPU持續(xù)高占用纤怒;
- 死鎖和主子線程搶鎖糯而;
RunLoop:
對于iOS開發(fā)來說,監(jiān)控卡頓就是要去找到主線程上都做了那些事肪跋。我們都知道歧蒋,線程的消息事件是依賴于NSRunLoop的,所以從NSRunLoop入手州既,就可以知道主線程上都調(diào)用了哪些方法谜洽,我們通過監(jiān)聽NSRunLoop的狀態(tài),就能發(fā)現(xiàn)調(diào)用方法是否執(zhí)行時間過長吴叶,從而判斷出是否會出現(xiàn)卡頓阐虚。
所以這里推薦的監(jiān)控卡頓的方案是:通過監(jiān)控RunLoop的狀態(tài)來判斷是否會出現(xiàn)卡頓。
RunLoop這個對象蚌卤,在iOS里是由CFRunLoop實現(xiàn)实束。簡單來說,RunLoop是用來監(jiān)聽輸入源逊彭,進(jìn)行調(diào)度處理的咸灿。這里的輸入源可以是輸入設(shè)備、網(wǎng)絡(luò)侮叮、周期性或者延遲時間避矢、異步回調(diào)。RunLoop會接受兩種類型的輸入源:一種是來自另一個線程或者來自不同應(yīng)用的異步消息囊榜;另一種是來自預(yù)定時間或者重復(fù)間隔的同步事件审胸。
RunLoop的目的是,當(dāng)有事件要去處理時保持線程忙卸勺,當(dāng)沒有事件要處理時讓線程進(jìn)入休眠砂沛。所以,了解RunLoop原理不光能夠運用到監(jiān)控卡頓上曙求,還可以提高用戶的交互體驗碍庵。通過將那些繁重而不緊急會大量占用CPU的任務(wù)(比如圖片加載)映企,放到空閑的RunLoop模式里執(zhí)行,就可以避開在UITrackingRunLoopMode這個RunLoop模式時執(zhí)行静浴。UIUITrackingRunLoopMode是用戶進(jìn)行滾動操作時會切換的RunLoop模式卑吭。避免在這個RunLoop模式執(zhí)行繁重的CPU任務(wù),就能避免影響用戶交互操作上的體驗马绝。
RunLoop的原理:
第一步
通知observers:Runloop要開始進(jìn)入loop了。緊接著就進(jìn)入loop挣菲。代碼如下:
//通知 observers
if (currentMode->_observerMask & kCFRunLoopEntry )
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
//進(jìn)入 loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
第二步
開啟一個do while來备坏荆活線程。通知Observers:RunLoop會觸發(fā)Timer回調(diào)白胀、Source0回調(diào)椭赋,接著執(zhí)行加入block。代碼如下:
// 通知 Observers RunLoop 會觸發(fā) Timer 回調(diào)
if (currentMode->_observerMask & kCFRunLoopBeforeTimers)
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 通知 Observers RunLoop 會觸發(fā) Source0 回調(diào)
if (currentMode->_observerMask & kCFRunLoopBeforeSources)
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 執(zhí)行 block
__CFRunLoopDoBlocks(runloop, currentMode);
接下來或杠,觸發(fā)Sourece0回調(diào),如果有Source1是ready狀態(tài)的話,就會跳轉(zhuǎn)到handle_msg去處理消息肚逸。代碼如下:
if (MACH_PORT_NULL != dispatchPort ) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
第三步
回調(diào)觸發(fā)后硕淑,通知到Observers:RunLoop的線程將進(jìn)入休眠(sleep)狀態(tài),代碼如下:
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
if (!poll && (currentMode->_observerMask & kCFRunLoopBeforeWaiting)) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
第四步
進(jìn)入休眠后挟鸠,會等待mach_port的消息叉信,以再次喚醒。只有在下面四個事件出現(xiàn)才會被再次喚醒:
- 基于port的Source事件艘希;
- Timer時間到硼身;
- RunLoop超時;
- 被調(diào)用者喚醒覆享。
等待喚醒的代碼如下:
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
// 基于 port 的 Source 事件佳遂、調(diào)用者喚醒
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
break;
}
// Timer 時間到、RunLoop 超時
if (currentMode->_timerFired) {
break;
}
} while (1);
第五步
喚醒時通知Observer:RunLoop的線程剛剛被喚醒了撒顿。代碼如下:
if (!poll && (currentMode->_observerMask & kCFRunLoopAfterWaiting))
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
第六步
RunLoop被喚醒后就要進(jìn)行處理消息了:
- 如果是Timer時間到的話丑罪,就觸發(fā)Timer的回調(diào);
- 如果是dispatch的話核蘸,就執(zhí)行block巍糯;
- 如果是Source1事件的話,就處理這個事件客扎。
消息執(zhí)行完后祟峦,就執(zhí)行加到loop里的block。代碼如下:
handle_msg:
// 如果 Timer 時間到徙鱼,就觸發(fā) Timer 回調(diào)
if (msg-is-timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
// 如果 dispatch 就執(zhí)行 block
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// Source1 事件的話宅楞,就處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
第七步
根據(jù)當(dāng)前RunLoop的狀態(tài)來判斷是否需要走下一個loop针姿。當(dāng)被外部強制停止或loop超時,就不再繼續(xù)下一個loop了厌衙,否則繼續(xù)走下一個loop距淫。代碼如下:
if (sourceHandledThisLoop && stopAfterHandle) {
// 事件已處理完
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
// 超時
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
// 外部調(diào)用者強制停止
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
// mode 為空,RunLoop 結(jié)束
retVal = kCFRunLoopRunFinished;
}
整個RunLoop的過程婶希,可以總結(jié)為如下所示的一張圖片榕暇。
CFRunLoop完整代碼:opensource.apple.com/source/CF/C…
RunLoop的六個狀態(tài)
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 進(jìn)入 loop
kCFRunLoopBeforeTimers , // 觸發(fā) Timer 回調(diào)
kCFRunLoopBeforeSources , // 觸發(fā) Source0 回調(diào)
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有狀態(tài)改變
}
如果RunLoop的線程,進(jìn)入睡眠前方法的執(zhí)行時間過長而導(dǎo)致無法進(jìn)入睡眠喻杈,或者線程喚醒后接受消息時間過長而無法進(jìn)入下一步的話彤枢,就可以認(rèn)為是線程受阻了筒饰。如果這個線程是主線程的話缴啡,表現(xiàn)就是出現(xiàn)卡頓。
所以瓷们,如果我們要利用RunLoop原理來監(jiān)控卡頓的話业栅,就要關(guān)注這兩個階段。RunLoop在進(jìn)入睡眠之前和喚醒后的兩個loop狀態(tài)定義的值谬晕,分別是kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting碘裕,也就是要觸發(fā)Source0回調(diào)和接受mac_port消息兩個狀態(tài)。
如何檢查卡頓固蚤?
首先需要創(chuàng)建一個CFRunLoopObserverContext觀察者娘汞,代碼如下:
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
將創(chuàng)建好的觀察者runLoopObserver添加到主線程RunLoop的common模式下觀察,然后夕玩,創(chuàng)建一個持續(xù)的子線程專門用來監(jiān)控主線程的RunLoop狀態(tài)你弦。
一旦發(fā)現(xiàn)進(jìn)入睡眠前的kCFRunLoopBeforeSources狀態(tài),或者喚醒后的狀態(tài)kCFRunLoopAfterWaiting燎孟,在設(shè)置的時間閾值內(nèi)一直沒有變化禽作,即可認(rèn)定為卡頓。
開啟子線程監(jiān)控的代碼如下:
//創(chuàng)建子線程監(jiān)控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子線程開啟一個持續(xù)的 loop 用來進(jìn)行監(jiān)控
while (YES) {
//semaphoreWait 信號等待的時間
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount = 0;
dispatchSemaphore = 0;
runLoopActivity = 0;
return;
}
//BeforeSources 和 AfterWaiting 這兩個狀態(tài)能夠檢測到是否卡頓
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
//將堆棧信息上報服務(wù)器的代碼放到這里
} //end activity
}// end semaphore wait
timeoutCount = 0;
}// end while
});
獲取堆棧信息使用PLCrashReporter.