使用RunLoop檢測卡頓

卡頓的原因:

  • 復(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é)為如下所示的一張圖片榕暇。

image

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.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揩页,一起剝皮案震驚了整個濱河市旷偿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爆侣,老刑警劉巖萍程,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異兔仰,居然都是意外死亡茫负,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進(jìn)店門乎赴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忍法,“玉大人潮尝,你說我怎么就攤上這事《鲂颍” “怎么了勉失?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長原探。 經(jīng)常有香客問我乱凿,道長,這世上最難降的妖魔是什么咽弦? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任告匠,我火速辦了婚禮,結(jié)果婚禮上离唬,老公的妹妹穿的比我還像新娘。我一直安慰自己划鸽,他們只是感情好输莺,可當(dāng)我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著裸诽,像睡著了一般嫂用。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丈冬,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天嘱函,我揣著相機與錄音,去河邊找鬼埂蕊。 笑死往弓,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蓄氧。 我是一名探鬼主播函似,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼喉童!你這毒婦竟也來了撇寞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤堂氯,失蹤者是張志新(化名)和其女友劉穎蔑担,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咽白,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡啤握,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了局扶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恨统。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡叁扫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出畜埋,到底是詐尸還是另有隱情莫绣,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布悠鞍,位于F島的核電站对室,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咖祭。R本人自食惡果不足惜掩宜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望么翰。 院中可真熱鬧牺汤,春花似錦、人聲如沸浩嫌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽码耐。三九已至追迟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間骚腥,已是汗流浹背敦间。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留束铭,地道東北人廓块。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像契沫,于是被迫代替她去往敵國和親剿骨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,969評論 2 355