iOS 性能優(yōu)化 - Runloop監(jiān)測卡頓分析(2)

前言

我們都知道,線程的消息事件是依賴于 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ā)者來分析并解決卡頓問題了。

參考:監(jiān)控卡頓完整代碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容