前言
我們都知道,線程的消息事件是依賴于 NSRunLoop 的,所以從 NSRunLoop 入手,就可以知道主線程上都調(diào)用了哪些方法。我們通過監(jiān)聽 NSRunLoop 的狀態(tài),就能夠發(fā)現(xiàn)調(diào)用方法是否執(zhí)行時間過長,從而判斷出是否會出現(xiàn)卡頓。
Runloop的運行原理。
首先了解一下Runloop的運行原理,如下圖所示:

第一步:
通知Observers: Runloop要開始runloop了。緊接著進入runloop啦
// 通知 observers
if (currentMode->_observerMask & kCFRunLoopEntry )
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 進入 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ā)Source0回調(diào),如果還有Source1是ready狀態(tài)的話,就會跳到hanlde_msg處理消息
if (MACH_PORT_NULL != dispatchPort ) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
第三步:
回調(diào)觸發(fā)后,通知Observes:Runloop的線程進入休眠狀態(tài)
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
if (!poll && (currentMode->_observerMask & kCFRunLoopBeforeWaiting)) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
等四步:
進入休眠后,會等待math_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);
第六步:
Runloop被喚醒后就要開始處理消息了
- 如果是Timer的時間的話,就處理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 的線程,進入睡眠前方法的執(zhí)行時間過長而導(dǎo)致無法進入睡眠,或者線程喚醒后接收消息時間過長而無法進入下一步的話,就可以認為是線程受阻了。如果這個線程是主線程的話,表現(xiàn)出來的就是出現(xiàn)了卡頓。
所以,如果我們要利用 RunLoop 原理來監(jiān)控卡頓的話,就是要關(guān)注這兩個階段。RunLoop 在進入睡眠之前和喚醒后的兩個 loop 狀態(tài)定義的值,分別是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要觸發(fā) Source0 回調(diào)和接收 mach_port 消息兩個狀態(tài)。
檢查卡頓
要想監(jiān)聽 RunLoop,你就首先需要創(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)進入睡眠前的 kCFRunLoopBeforeSources 狀態(tài),或者喚醒后的狀態(tài) kCFRunLoopAfterWaiting,在設(shè)置的時間閾值內(nèi)一直沒有變化,即可判定為卡頓。
開啟一個子線程監(jiān)控的代碼如下:
// 創(chuàng)建子線程監(jiān)控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子線程開啟一個持續(xù)的 loop 用來進行監(jiān)控
while (YES) {
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
});
我們把這個閾值設(shè)置成了 3 秒。那么,這個 3 秒的閾值是從何而來呢?這樣設(shè)置合理嗎?
其實,觸發(fā)卡頓的時間閾值,我們可以根據(jù) WatchDog 機制來設(shè)置。WatchDog 在不同狀態(tài)下設(shè)置的不同時間,如下所示:
- 啟動(Launch):20s;
- 恢復(fù)(Resume):10s;
- 掛起(Suspend):10s;
- 退出(Quit):6s;
-
后臺(Background):3min(在 iOS 7 之前,每次申請 10min; 之后改為每次申請 3min,可連續(xù)申請,最多申請到 10min)。
通過 WatchDog 設(shè)置的時間,我認為可以把啟動的閾值設(shè)置為 10 秒,其他狀態(tài)則都默認設(shè)置為 3 秒??偟脑瓌t就是,要小于 WatchDog 的限制時間。當(dāng)然了,這個閾值也不用小得太多,原則就是要優(yōu)先解決用戶感知最明顯的體驗問題。
獲取卡頓的方法堆棧信息
子線程監(jiān)控發(fā)現(xiàn)卡頓后,還需要記錄當(dāng)前出現(xiàn)卡頓的方法堆棧信息,并適時推送到服務(wù)端供開發(fā)者分析,從而解決卡頓問題。那么,在這個過程中,如何獲取卡頓的方法堆棧信息呢?
獲取堆棧信息的一種方法是直接調(diào)用系統(tǒng)函數(shù)。這種方法的優(yōu)點在于,性能消耗小。但是,它只能夠獲取簡單的信息,也沒有辦法配合 dSYM 來獲取具體是哪行代碼出了問題,而且能夠獲取的信息類型也有限。這種方法,因為性能比較好,所以適用于觀察大盤統(tǒng)計卡頓情況,而不是想要找到卡頓原因的場景。
第一種:直接調(diào)用系統(tǒng)函數(shù)方法的主要思路是:用 signal 進行錯誤信息的獲取。
第二種:直接用 PLCrashReporter 這個開源的第三方庫來獲取堆棧信息。這種方法的特點是,能夠定位到問題代碼的具體位置,而且性能消耗也不大。所以,也是我推薦的獲取堆棧信息的方法。
搜集到卡頓的方法堆棧信息以后,就是由開發(fā)者來分析并解決卡頓問題了。