iOS 利用 RunLoop 原理去監(jiān)控卡頓

本文是借鑒 戴銘老師 iOS開發(fā)高手課 內(nèi)容總結(jié)。

目錄

1、卡頓問題

2、RunLoop介紹

3、RunLoop執(zhí)行過程 介紹

4、RunLoop全部六個狀態(tài)

5、RunLoop監(jiān)控卡頓操作?

6、直接用 PLCrashReporter這個開源的第三方庫來獲取堆棧信息

7、微信開源?matrix-ios卡頓監(jiān)控?工具

8、騰訊 Bugly 工具?Bugly?: 可監(jiān)控?App在運行過程中發(fā)生的 【崩潰、卡頓、ANR、錯誤】

總結(jié)監(jiān)控卡頓Demo:Demo

1、卡頓問題:卡頓問題,就是在主線程上無法響應用戶交互的問題。如果一個 App 時不時地就給你卡一下,有時還長時間無響應

1、卡頓根源:

? ? ? ? 1>復雜 UI 、圖文混排的繪制量過大;

? ? ? ? 2>在主線程上做網(wǎng)絡同步請求;

? ? ? ? 3>在主線程做大量的 IO 操作;

? ? ? ? 4>運算量過大,CPU 持續(xù)高占用;

? ? ? ? 5>死鎖和主子線程搶鎖。

2、FPS:FPS 是一秒顯示的幀數(shù),也就是一秒內(nèi)畫面變化數(shù)量。如果按照動畫片來說,動畫片的 FPS 就是 24,是達不到 60 滿幀的。也就是說,對于動畫片來說,24 幀時雖然沒有 60 幀時流暢,但也已經(jīng)是連貫的了,所以并不能說 24 幀時就算是卡住了。由此可見,簡單地通過監(jiān)視 FPS 是很難確定是否會出現(xiàn)卡頓問題了。

2、RunLoop介紹(推薦的監(jiān)控卡頓的方案是:通過監(jiān)控 RunLoop 的狀態(tài)來判斷是否會出現(xiàn)卡頓。)

1、RunLoop原理:對于 iOS 開發(fā)來說,監(jiān)控卡頓就是要去找到主線程上都做了哪些事兒。我們都知道,線程的消息事件是依賴于 NSRunLoop 的,所以從 NSRunLoop 入手,就可以知道主線程上都調(diào)用了哪些方法。我們通過【監(jiān)聽 NSRunLoop 的狀態(tài),就能夠發(fā)現(xiàn)調(diào)用方法是否執(zhí)行時間過長】,從而判斷出是否會出現(xiàn)卡頓。

2、RunLoop 是 iOS 開發(fā)中的一個基礎概念,它可以做哪些事兒,以及它為什么可以做成這些事兒?

RunLoop 這個對象,在 iOS 里由 CFRunLoop 實現(xiàn)。

【簡單來說,RunLoop 是用來監(jiān)聽輸入源,進行調(diào)度處理的】。這里的輸入源可以是輸入設備、網(wǎng)絡、周期性或者延遲時間、異步回調(diào)。

RunLoop 會接收兩種類型的輸入源:一種是來自另一個線程或者來自不同應用的異步消息;另一種是來自預訂時間或者重復間隔的同步事件。

【RunLoop 的目的是,當有事件要去處理時保持線程忙,當沒有事件要處理時讓線程進入休眠?!?/p>

所以,了解 RunLoop 原理不光能夠運用到監(jiān)控卡頓上,還可以提高用戶的交互體驗。通過將那些繁重而不緊急會大量占用 CPU 的任務(比如圖片加載),放到空閑的 RunLoop 模式里執(zhí)行,就可以避開在 UITrackingRunLoopMode 這個 RunLoop 模式時是執(zhí)行。UITrackingRunLoopMode 是用戶進行滾動操作時會切換到的 RunLoop 模式,避免在這個 RunLoop 模式執(zhí)行繁重的 CPU 任務,就能避免影響用戶交互操作上體驗。

3、RunLoop執(zhí)行過程 介紹

1、第一步通知 observers:RunLoop 要開始進入 loop 了。緊接著就進入 loop

1

2、第二步開啟一個 do while 來?;罹€程。通知 Observers:RunLoop 會觸發(fā) Timer 回調(diào)、Source0 回調(diào),接著執(zhí)行加入的 block。代碼如下:

2-0

接下來,觸發(fā) Source0 回調(diào),如果有 Source1 是 ready 狀態(tài)的話,就會跳轉(zhuǎn)到 handle_msg 去處理消息。代碼如下:

2-1

3、第三步回調(diào)觸發(fā)后,通知 Observers:RunLoop 的線程將進入休眠(sleep)狀態(tài)。代碼如下:

3

4、第四步進入休眠后,會等待 mach_port 的消息,以再次喚醒。只有在下面四個事件出現(xiàn)時才會被再次喚醒:

? ? 1>基于 port 的 Source 事件;

? ? ?2>Timer 時間到;

? ? ?3>RunLoop 超時;

? ? ?4>被調(diào)用者喚醒。

等待喚醒的代碼如下:

4

5、第五步喚醒時通知 Observer:RunLoop 的線程剛剛被喚醒了。代碼如下:

5

6、第六步RunLoop 被喚醒后就要開始處理消息了:

? ? ? ?1>如果是 Timer 時間到的話,就觸發(fā) Timer 的回調(diào);

? ? ? ?2>如果是 dispatch 的話,就執(zhí)行 block;

? ? ? ?3>如果是 source1 事件的話,就處理這個事件。消息執(zhí)行完后,就執(zhí)行加到 loop 里的 block。代碼如下:

6

7、第七步根據(jù)當前 RunLoop 的狀態(tài)來判斷是否需要走下一個 loop。當被外部強制停止或 loop 超時時,就不繼續(xù)下一個 loop 了,否則繼續(xù)走下一個 loop 。代碼如下:

7

整個 RunLoop 過程,我們可以總結(jié)為如下所示的一張圖片。RunLoop全部代碼過程

RunLoop全部流程


4、RunLoop全部六個狀態(tài)

loop 的六個狀態(tài)通過對 RunLoop 原理的分析,我們可以看出在整個過程中,loop 的狀態(tài)包括 6 個,其代碼定義如下:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {?

? ? ? ? ?kCFRunLoopEntry , // 進入 loop?

? ? ? ? ?kCFRunLoopBeforeTimers , // 觸發(fā) Timer 回調(diào)?

? ? ? ? ?kCFRunLoopBeforeSources , // 觸發(fā) Source0 回調(diào)?

? ? ? ? ?kCFRunLoopBeforeWaiting , // 等待 mach_port 消息?

? ? ? ? ?kCFRunLoopAfterWaiting , // 接收 mach_port 消息?

? ? ? ? ?kCFRunLoopExit , // 退出 loop?

? ? ? ? ?kCFRunLoopAllActivities // loop 所有狀態(tài)改變}

分析:如果 RunLoop 的線程,【進入睡眠前方法的執(zhí)行時間過長而導致無法進入睡眠】,或者 【線程喚醒后接收消息時間過長而無法進入下一步的話】,就可以認為是 ——> 線程受阻了?!救绻@個線程是主線程的話,表現(xiàn)出來的就是出現(xiàn)了卡頓】。

如果我們要利用 RunLoop 原理來監(jiān)控卡頓的話,就是要關注這兩個階段。RunLoop 在進入睡眠之前和喚醒后的兩個 loop 狀態(tài)定義的值,分別是 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting ,也就是要觸發(fā) Source0 回調(diào)和接收 mach_port 消息兩個狀態(tài)。

5、RunLoop監(jiān)控卡頓操作 (參考資料)、騰訊matirx 框架?matirx? 、 或者?Gitee倉庫Matrix

1、開啟一個子線程監(jiān)控的代碼如下:代碼中的 NSEC_PER_SEC,代表的是觸發(fā)卡頓的時間閾值,單位是秒??梢钥吹?,我們把這個閾值設置成了 3 秒。那么,這個 3 秒的閾值是從何而來呢?這樣設置合理嗎?其實,觸發(fā)卡頓的時間閾值,我們可以根據(jù) WatchDog 機制來設置。WatchDog 在不同狀態(tài)下設置的不同時間,如下所示:啟動(Launch):20s;恢復(Resume):10s;掛起(Suspend):10s;退出(Quit):6s;后臺(Background):3min(在 iOS 7 之前,每次申請 10min; 之后改為每次申請 3min,可連續(xù)申請,最多申請到 10min)。通過 WatchDog 設置的時間,我認為可以把啟動的閾值設置為 10 秒,其他狀態(tài)則都默認設置為 3 秒??偟脑瓌t就是,要小于 WatchDog 的限制時間。當然了,這個閾值也不用小得太多,原則就是要優(yōu)先解決用戶感知最明顯的體驗問題。

2、如何獲取卡頓的方法堆棧信息?

子線程監(jiān)控發(fā)現(xiàn)卡頓后,還需要記錄當前出現(xiàn)卡頓的方法堆棧信息,并適時推送到服務端供開發(fā)者分析,從而解決卡頓問題。那么,在這個過程中,如何獲取卡頓的方法堆棧信息呢?

獲取堆棧信息的一種方法是直接調(diào)用系統(tǒng)函數(shù)。

這種方法的優(yōu)點在于,性能消耗小。但是,它只能夠獲取簡單的信息,也沒有辦法配合 dSYM 來獲取具體是哪行代碼出了問題,而且能夠獲取的信息類型也有限。這種方法,因為性能比較好,所以適用于觀察大盤統(tǒng)計卡頓情況,而不是想要找到卡頓原因的場景。

直接調(diào)用系統(tǒng)函數(shù)方法的主要思路是:用 signal 進行錯誤信息的獲取

6、直接用 PLCrashReporter這個開源的第三方庫來獲取堆棧信息

具體如何使用 PLCrashReporter 來獲取堆棧信息,代碼如下所示:

// 獲取數(shù)據(jù)

?NSData *lagData = [[[PLCrashReporter alloc] initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];?

// 轉(zhuǎn)換成 PLCrashReport 對象?

PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];?

// 進行字符串格式化處理?

NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];

?//將字符串上傳服務器NSLog(@"lag happen, detail below: \n %@",lagReportString);

7、微信開源?matrix-ios卡頓監(jiān)測?https://github.com/tencent/matrix/tree/master/matrix/matrix-iOS? https://github.com/Tencent/matrix?工具

微信團隊就放出了一篇文章專門介紹卡頓監(jiān)控方案“微信 iOS 卡頓監(jiān)控系統(tǒng)”鏈接。之后,很多團隊參照這篇文章開發(fā)了自己的卡頓監(jiān)控系統(tǒng)。

1> 今年的 4 月 3 號,微信團隊將他們的卡頓監(jiān)控系統(tǒng)matrix開源出來了,包括 Matrix for iOS / MacOS https://github.com/Tencent/matrix/tree/master/matrix/matrix-iOS?和Android系統(tǒng)的監(jiān)控方案。關于 matrix-iOS 的卡頓監(jiān)控原理,你可以點擊這個鏈接?https://github.com/Tencent/matrix/wiki/Matrix-for-iOS-macOS-卡頓監(jiān)控原理?查看。

如果你的 App 現(xiàn)在還沒有卡頓監(jiān)控系統(tǒng),可以考慮直接集成 matrix-iOS,直接在 Podfile 里添加 pod ‘matrix-wechat’ 就可以了。如果已經(jīng)有了卡頓監(jiān)控系統(tǒng),我建議你閱讀下 matrix-iOS 的代碼,里面有很多細節(jié)值得我們學習。

比如:子線程監(jiān)控檢測時間間隔:matrix-iOS 監(jiān)控卡頓的子線程是通過 NSThread 創(chuàng)建的,檢測時間間隔正常情況是 1 秒,在出現(xiàn)卡頓情況下,間隔時間會受檢測線程退火算法影響,按照斐波那契數(shù)列遞增,直到?jīng)]有卡頓時恢復為 1 秒。

2> 子線程監(jiān)控退火算法:避免一個卡頓會寫入多個文件的情況。

【為了降低檢測帶來的性能損耗,我們?yōu)闄z測線程增加了退火算法:

每次子線程檢查到主線程卡頓,會先獲得主線程的堆棧并保存到內(nèi)存中(不會直接去獲得線程快照保存到文件中);

將獲得的主線程堆棧與上次卡頓獲得的主線程堆棧進行比對:如果堆棧不同,則獲得當前的線程快照并寫入文件中;

如果相同則會跳過,并按照斐波那契數(shù)列將檢查時間遞增直到?jīng)]有遇到卡頓或者主線程卡頓堆棧不一樣。

這樣,可以避免同一個卡頓寫入多個文件的情況;避免檢測線程遇到主線程卡死的情況下,不斷寫線程快照文件?!?/p>

3> RunLoop 卡頓時間閾值設置:對于 RunLoop 超時閾值的設置,微信設置的是 2 秒。CPU 使用率閾值設置:當單核 CPU 使用率超過 80%,就判定 CPU 占用過高。CPU 使用率過高,可能導致 App 卡頓。

【Matrix 卡頓監(jiān)控在 Runloop 的起始 最開始和結(jié)束最末尾 ?位置添加 Observer,從而獲得主線程的開始和結(jié)束狀態(tài)??D監(jiān)控起一個子線程定時檢查主線程的狀態(tài),當主線程的狀態(tài)運行超過一定閾值則認為主線程卡頓,從而標記為一個卡頓。目前微信使用的卡頓監(jiān)控,主程序 Runloop 超時的閾值是 2 秒,子線程的檢查周期是 1 秒。每隔 1 秒,子線程檢查主線程的運行狀態(tài);如果檢查到主線程 Runloop 運行超過 2 秒則認為是卡頓,并獲得當前的線程快照。同時,我們也認為 CPU 過高也可能導致應用出現(xiàn)卡頓,所以在子線程檢查主線程狀態(tài)的同時,如果檢測到 CPU 占用過高,會捕獲當前的線程快照保存到文件中。目前微信應用中認為,單核 CPU 的占用超過了 80%,此時的 CPU 占用就過高了?!?/p>

4> 線程過多時 CPU 在切換線程上下文時,還會更新寄存器,更新寄存器時需要尋址,而尋址的過程還會有較大的 CPU 消耗。按照微信團隊的經(jīng)驗,線程數(shù)超出 64 個時會導致主線程卡頓,如果卡頓是由于線程多造成的,那么就沒必要通過獲取主線程堆棧去找卡頓原因了。根據(jù) matrix-iOS 的實測,每隔 50 毫秒獲取主線程堆棧會增加 3% 的 CPU 占用,所以當檢測到主線程卡頓以后,我們需要先判斷是否是因為線程數(shù)過多導致的,而不是一有卡頓問題就去獲取主線程堆棧。

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

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

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