本文是借鑒 戴銘老師 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僧免、卡頓問題:卡頓問題,就是在主線程上無法響應(yīng)用戶交互的問題捏浊。如果一個 App 時不時地就給你卡一下懂衩,有時還長時間無響應(yīng)
1、卡頓根源:
? ? ? ? 1>復雜 UI 、圖文混排的繪制量過大浊洞;
? ? ? ? 2>在主線程上做網(wǎng)絡(luò)同步請求牵敷;
? ? ? ? 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ā)中的一個基礎(chǔ)概念叨叙,它可以做哪些事兒锭弊,以及它為什么可以做成這些事兒?
RunLoop 這個對象擂错,在 iOS 里由 CFRunLoop 實現(xiàn)廷蓉。
【簡單來說,RunLoop 是用來監(jiān)聽輸入源,進行調(diào)度處理的】桃犬。這里的輸入源可以是輸入設(shè)備刹悴、網(wǎng)絡(luò)、周期性或者延遲時間攒暇、異步回調(diào)土匀。
RunLoop 會接收兩種類型的輸入源:一種是來自另一個線程或者來自不同應(yīng)用的異步消息;另一種是來自預訂時間或者重復間隔的同步事件形用。
【RunLoop 的目的是就轧,當有事件要去處理時保持線程忙,當沒有事件要處理時讓線程進入休眠田度《视】
所以,了解 RunLoop 原理不光能夠運用到監(jiān)控卡頓上镇饺,還可以提高用戶的交互體驗乎莉。通過將那些繁重而不緊急會大量占用 CPU 的任務(wù)(比如圖片加載),放到空閑的 RunLoop 模式里執(zhí)行奸笤,就可以避開在 UITrackingRunLoopMode 這個 RunLoop 模式時是執(zhí)行惋啃。UITrackingRunLoopMode 是用戶進行滾動操作時會切換到的 RunLoop 模式,避免在這個 RunLoop 模式執(zhí)行繁重的 CPU 任務(wù)监右,就能避免影響用戶交互操作上體驗边灭。
3、RunLoop執(zhí)行過程 介紹
1健盒、第一步通知 observers:RunLoop 要開始進入 loop 了绒瘦。緊接著就進入 loop
2、第二步開啟一個 do while 來笨垩ⅲ活線程椭坚。通知 Observers:RunLoop 會觸發(fā) Timer 回調(diào)、Source0 回調(diào)搏色,接著執(zhí)行加入的 block善茎。代碼如下:
接下來,觸發(fā) Source0 回調(diào)频轿,如果有 Source1 是 ready 狀態(tài)的話垂涯,就會跳轉(zhuǎn)到 handle_msg 去處理消息。代碼如下:
3航邢、第三步回調(diào)觸發(fā)后耕赘,通知 Observers:RunLoop 的線程將進入休眠(sleep)狀態(tài)。代碼如下:
4膳殷、第四步進入休眠后操骡,會等待 mach_port 的消息,以再次喚醒。只有在下面四個事件出現(xiàn)時才會被再次喚醒:
? ? 1>基于 port 的 Source 事件册招;
? ? ?2>Timer 時間到岔激;
? ? ?3>RunLoop 超時;
? ? ?4>被調(diào)用者喚醒是掰。
等待喚醒的代碼如下:
5虑鼎、第五步喚醒時通知 Observer:RunLoop 的線程剛剛被喚醒了。代碼如下:
6键痛、第六步RunLoop 被喚醒后就要開始處理消息了:
? ? ? ?1>如果是 Timer 時間到的話炫彩,就觸發(fā) Timer 的回調(diào);
? ? ? ?2>如果是 dispatch 的話絮短,就執(zhí)行 block江兢;
? ? ? ?3>如果是 source1 事件的話,就處理這個事件丁频。消息執(zhí)行完后杉允,就執(zhí)行加到 loop 里的 block。代碼如下:
7限府、第七步根據(jù)當前 RunLoop 的狀態(tài)來判斷是否需要走下一個 loop夺颤。當被外部強制停止或 loop 超時時痢缎,就不繼續(xù)下一個 loop 了胁勺,否則繼續(xù)走下一個 loop 。代碼如下:
整個 RunLoop 過程独旷,我們可以總結(jié)為如下所示的一張圖片署穗。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)控卡頓的話懂盐,就是要關(guān)注這兩個階段。RunLoop 在進入睡眠之前和喚醒后的兩個 loop 狀態(tài)定義的值糕档,分別是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 莉恼,也就是要觸發(fā) Source0 回調(diào)和接收 mach_port 消息兩個狀態(tài)。
5、RunLoop監(jiān)控卡頓操作 (參考資料)俐银、騰訊matirx 框架?matirx? 尿背、 或者?Gitee倉庫Matrix
1、開啟一個子線程監(jiān)控的代碼如下:代碼中的 NSEC_PER_SEC悉患,代表的是觸發(fā)卡頓的時間閾值残家,單位是秒∈墼辏可以看到坞淮,我們把這個閾值設(shè)置成了 3 秒。那么陪捷,這個 3 秒的閾值是從何而來呢回窘?這樣設(shè)置合理嗎?其實市袖,觸發(fā)卡頓的時間閾值啡直,我們可以根據(jù) WatchDog 機制來設(shè)置。WatchDog 在不同狀態(tài)下設(shè)置的不同時間苍碟,如下所示:啟動(Launch):20s酒觅;恢復(Resume):10s;掛起(Suspend):10s微峰;退出(Quit):6s舷丹;后臺(Background):3min(在 iOS 7 之前,每次申請 10min蜓肆; 之后改為每次申請 3min颜凯,可連續(xù)申請,最多申請到 10min)仗扬。通過 WatchDog 設(shè)置的時間症概,我認為可以把啟動的閾值設(shè)置為 10 秒,其他狀態(tài)則都默認設(shè)置為 3 秒早芭”顺牵總的原則就是,要小于 WatchDog 的限制時間退个。當然了募壕,這個閾值也不用小得太多,原則就是要優(yōu)先解決用戶感知最明顯的體驗問題帜乞。
2司抱、如何獲取卡頓的方法堆棧信息?
子線程監(jiān)控發(fā)現(xiàn)卡頓后黎烈,還需要記錄當前出現(xiàn)卡頓的方法堆棧信息习柠,并適時推送到服務(wù)端供開發(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];
?//將字符串上傳服務(wù)器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)控方案缸逃。關(guā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)]有遇到卡頓或者主線程卡頓堆棧不一樣。
這樣外盯,可以避免同一個卡頓寫入多個文件的情況摘盆;避免檢測線程遇到主線程卡死的情況下,不斷寫線程快照文件饱苟÷獬海】
3> RunLoop 卡頓時間閾值設(shè)置:對于 RunLoop 超時閾值的設(shè)置,微信設(shè)置的是 2 秒掷空。CPU 使用率閾值設(shè)置:當單核 CPU 使用率超過 80%肋殴,就判定 CPU 占用過高拗秘。CPU 使用率過高床嫌,可能導致 App 卡頓。
【Matrix 卡頓監(jiān)控在 Runloop 的起始 最開始和結(jié)束最末尾 ?位置添加 Observer怒详,從而獲得主線程的開始和結(jié)束狀態(tài)酿傍±优常卡頓監(jiān)控起一個子線程定時檢查主線程的狀態(tài),當主線程的狀態(tài)運行超過一定閾值則認為主線程卡頓赤炒,從而標記為一個卡頓氯析。目前微信使用的卡頓監(jiān)控,主程序 Runloop 超時的閾值是 2 秒莺褒,子線程的檢查周期是 1 秒掩缓。每隔 1 秒,子線程檢查主線程的運行狀態(tài)遵岩;如果檢查到主線程 Runloop 運行超過 2 秒則認為是卡頓你辣,并獲得當前的線程快照。同時尘执,我們也認為 CPU 過高也可能導致應(yīng)用出現(xiàn)卡頓舍哄,所以在子線程檢查主線程狀態(tài)的同時,如果檢測到 CPU 占用過高誊锭,會捕獲當前的線程快照保存到文件中表悬。目前微信應(yīng)用中認為,單核 CPU 的占用超過了 80%丧靡,此時的 CPU 占用就過高了蟆沫〔婕ィ】
4> 線程過多時 CPU 在切換線程上下文時,還會更新寄存器饥追,更新寄存器時需要尋址图仓,而尋址的過程還會有較大的 CPU 消耗。按照微信團隊的經(jīng)驗但绕,線程數(shù)超出 64 個時會導致主線程卡頓救崔,如果卡頓是由于線程多造成的,那么就沒必要通過獲取主線程堆棧去找卡頓原因了捏顺。根據(jù) matrix-iOS 的實測六孵,每隔 50 毫秒獲取主線程堆棧會增加 3% 的 CPU 占用,所以當檢測到主線程卡頓以后幅骄,我們需要先判斷是否是因為線程數(shù)過多導致的劫窒,而不是一有卡頓問題就去獲取主線程堆棧。