主要是主線程阻塞。在開發過程當中,遇到的形成主線程阻塞的緣由多是:html
Matrix 卡頓監控在 RunLoop 的起始最開始和結束最末尾位置添加 Observer,從而得到主線程的開始和結束狀態。卡頓監控起一個子線程定時檢查主線程的狀態,當主線程的狀態運行超過必定閾值則認爲主線程卡頓,從而標記爲一個卡頓。ios
採用兩個準則:git
微信公開使用的卡頓監控中,主程序 Runloop 超時的閾值是 2 秒,子線程的檢查週期是 1 秒。每隔 1 秒,子線程檢查主線程的運行狀態;若是檢查到主線程 Runloop 運行超過 2 秒則認爲是卡頓,並得到當前的線程快照。同時,微信團隊也認爲 CPU 太高也可能致使應用出現卡頓,因此在子線程檢查主線程狀態的同時,若是檢測到 CPU 佔用太高,會捕獲當前的線程快照保存到文件中。目前微信應用中認爲,單核 CPU 的佔用超過了 80%,此時的 CPU 佔用就太高了。github
爲了下降檢測帶來的性能損耗,爲檢測線程增長了退火算法:算法
子線程檢測到主線程 Runloop 時,會得到當前的線程快照當作卡頓文件,但當前的主線程堆棧不必定是最耗時的堆棧,不必定是致使主線程超時的主要緣由。Matrix 卡頓監控經過主線程耗時堆棧提取來解決這個問題。 卡頓監控定時獲取主線程堆棧,並將堆棧保存到內存的一個循環隊列中。以下圖,每間隔時間 t 得到一個堆棧,而後將堆棧保存到一個最大個數爲 3 的循環隊列中。有一個遊標不斷的指向最近的堆棧。 微信的策略是每隔 50 毫秒獲取一次主線程堆棧,保存最近 20 個主線程堆棧。這個會增長 3% 的 CPU 佔用,內存佔用能夠忽略不計。bash
當主線程檢測到卡頓時,經過對保存到循壞隊列中的堆棧進行回溯,獲取最近最耗時堆棧。 以下圖,檢測到卡頓時,內存的循環隊列中記錄了最近的20個主線程堆棧,須要從中找出最近最耗時的堆棧。Matrix 卡頓監控用以下特徵找出最近最耗時堆棧:微信
卡死檢測流程以下網絡
卡頓監控須要仔細定義本身的分類規則。能夠是從調用堆棧的最外層開始歸類,或者是取中間一部分歸類,或者是取最裏面一部分歸類。各有優缺點:數據結構
微信採用了最內層歸類的優化版,亦即進行二級歸類。 第一級按照最內倒數2層歸類,這樣可以將同一緣由的卡頓集中起來; 第二級分類是從第一級點擊進來,而後從最內層倒數4層進行歸類,這樣可以將同一緣由的不一樣業務分散歸類起來。ide
灰度收集到的結果是用戶平均天天會產生30個 dump 文件,壓縮上傳大約要 300k 流量。預計正式發佈的話會對後臺有比較大的壓力,對用戶也有必定流量損耗。因此必須進行抽樣上報。
很容易想到經過檢測FPS就能夠知道App是否發生了卡頓,也可以經過一段連續的FPS幀數計算丟幀率來衡量當前頁面繪製的質量。然而實踐發現FPS的刷新頻率很是快,而且容易發生抖動,所以直接經過比較經過FPS來偵測卡頓是比較困難的。而檢測主線程消息循環執行的時間就要容易的多了,這也是業內經常使用的一種檢測卡頓的方法。所以,Hertz在實踐中採用的就是檢測主線程每次執行消息循環的時間,當這一時間大於閾值時,就記爲發生一次卡頓。
有的卡頓連續性耗時較長,例如打開新頁面時的卡頓;而有的卡頓連續性耗時相對較短但頻次較快,例如列表滑動時的卡頓。所以,採用了「N次卡頓超過閾值T」的斷定策略,即一個時間段內卡頓的次數累計大於N時才觸發採集和上報:例如卡頓閾值T=2000ms、卡頓次數N=1,能夠斷定爲單次耗時較長的卡頓;而卡頓閾值T=300ms、卡頓次數N=5,能夠斷定爲頻次較快的卡頓。
第一個問題是堆棧抓取的時機。抓取堆棧的時機必須是在卡頓發生當時,而不是以後,不然不能準確抓到形成卡頓的代碼,所以在子線程中當卡頓尚未結束時抓取堆棧。 第二個問題是堆棧如何歸類,卡頓堆棧的歸類和Crash堆棧不一樣,以最內層代碼歸類顯然是不合適的,由於外層不一樣的業務邏輯代碼在最內層的調用堆棧有多是相同的。以最外層代碼歸類也是不合適的,由於最外層代碼有多是業務邏輯代碼,也有多是系統調用。採用最內層歸類的原則,並匹配一些簡單的規則,以命中規則的類名來歸類。
檢查卡頓的依據和上報時機(bugly.qq.com/docs/user-g… 依據是監控主線程 Runloop 的執行,觀察執行耗時是否超過預約閥值(默認閥值爲3000ms) ,若是監控到卡頓時會當即記錄線程堆棧到本地,在App從後臺切換到前臺時,執行上報。
如何監控卡頓
MTHawkeye是美圖開源的卡頓監控,看了源碼設計思想跟微信的Matrix差很少,不過有不少技術的沉澱,應該是爲美圖定製,設計很值得研究。
業界大部分的監控方案大同小異,基於監聽RunLoop的通知狀態,開啓常駐子線程定時檢測主線程的RunLoop狀態切換是否存在超時,超時則記爲一次卡頓,當卡頓時長超過設定的閾值dump堆棧,進行相關策略處理以後在合適的時間上報。Matrix爲騰訊最新的開源庫,其堆棧處理策略較好,目前備受歡迎。
FPS(Frames Per Second)表示頁面每秒的幀數,FPS越高代表頁面越流暢,值50~60之間是比較流暢的,反之低於會卡頓。FPS經過藉助CADisplayLink在一個週期的計數間接表示。例外,根據可滑動界面在滑動狀態RunLoop由kCFRunLoopDefaultMode切換成UITrackingRunLoopMode能夠區分頁面不流暢產生的場景是否在滾動過程。
CADisplayLink是以跟IOS設備相同屏幕刷新頻率(每秒60幀)的定時器,經過添加一個target和綁定selector,以NSRunLoopCommonModes模式將該定時器註冊到RunLoop,屏幕收到每一幀的刷屏通知同時調用target綁定的selector計數操做,獲取時間戳大於1秒的計數爲當前頁面的FPS。
實現原理:ping經常使用於測試網絡測試數據包可否到達ip地址進而測試網絡應答。固然用來監控卡頓監控,主要的核心思想是開啓子線程維護一個ping定時器,經過固定時間片斷ping主線程(發送一個通知),若是主線程不是繁忙狀態會收到通知並pong迴應(回送一個通知給子線程),不然子線程超過設定的pong定時閾值,沒有收到主線程pong回覆則斷定爲是卡頓了,而後dump堆棧下來。
實現原理:開啓一個子線程,監聽RunLoop的通知狀態,若是在設定的卡頓閾值時間內沒有收到RunLoop的通知狀態,那麼就斷定爲主線程卡頓了,而後dump堆棧,反之沒有卡頓,能夠記錄卡頓的頻次,到達必定的頻次再上報。
oc每一個方法的調用最終都是轉成objc_msgSend方式通知消息,經過維護一個數據結構統計每一個方法的調用時長進行性能分析,可是這樣很是損耗性能,維護成本比較高,不推薦使用。
最後,決定採起監聽RunLoop的方式,參考Matrix和MTHawkeye。
在 RunLoop 的起始最開始和結束最末尾位置添加 Observer,從而得到主線程的開始和結束狀態的耗時。卡頓監控起一個子線程定時檢查主線程的狀態(默認200ms),當主線程的狀態運行耗時超過必定閾值(默認400ms)則認爲主線程卡頓,從而標記爲一個卡頓。若是卡頓時長超過8秒,則斷定爲卡死。卡頓閾值和檢測線程的週期直接影響卡頓監控的能力和性能損耗。
系統提供task_threads方法獲取task的全部線程,每個線程的信息能夠經過thread_get_state方法獲取到,信息填充在 _STRUCT_MCONTEXT 類型的參數中,經過這個參數能夠取到當前線程的Stack Pointer和Frame Pointer,而後回溯整個函數的調用棧找到全部函數的地址,經過偏移計算出物理地址,最後再進行符號化取得函數名。
採用了退火算法一部分過濾連續相同堆棧
ThreadBacktraceSnapshot *mainBacktraceSnapshot = [self generateBacktraceSnapshot:dumpType];
ThreadBacktraceSnapshot *preSnapshot = self.snapshotsArray.lastObject;
if (preSnapshot) {
if (![preSnapshot.backtraceDescription isEqualToArray:mainBacktraceSnapshot.backtraceDescription]) {
mainBacktraceSnapshot.capturedCount = self.annealingCount;
[self.snapshotsArray addObject:mainBacktraceSnapshot];
self.annealingCount = 1;
} else {
self.annealingCount += 1;
}
} else {
self.annealingCount = 1;
[self.snapshotsArray addObject:mainBacktraceSnapshot];
}
複製代碼
上述的實現方案能夠記錄到卡住的時間,業務能夠定製卡頓多長時間則斷定爲卡死的時間。
當頁面卡頓時長超過卡死的閾值,在這個閾值的基礎上在計時,直到RunLoop進入下一個狀態計時結束,不然會一直等到觸發Watch
/**
設定卡頓閾值和檢測卡頓線程檢測時間間隔啓動監控
@param runloopTimeOut 卡頓閾值
@param checkRunLoopTimeOutThreshold 檢測卡頓線程檢測時間間隔
*/
- (void)startWithRunloopTimeOut:(useconds_t)runloopTimeOut andCheckPeriodTime:(unsigned)checkRunLoopTimeOutThreshold;
/**
採用默認值啓動,卡頓閾值400ms,每隔200ms檢查一次
*/
- (void)start;
- (void)stop;
複製代碼
CPU波動2%-4% Dog監控閾值將APP殺死,用這種方式即便最終發生了閃退也能夠逼近實際的卡死時間,偏差暫未有結論。
目前卡頓監控只是應用在demo中,沒有在線上使用過,應該會有不少問題,例如性能瓶頸、堆棧過濾、退火算法優化等待。