iOS監控:卡頓檢測

林欣達發表於2017-03-27

前言

iOS監控:卡頓檢測

在很早之前就有過實現一套自己的iOS監控體系,但首先是instrument足夠的優秀,幾乎所有監控相關的操作都有對應的工具。二來,也是筆者沒(lan)時(de)間(zuo),專案大多也整合了第三方的統計SDK,所以遲遲沒有去實現。這段時間,因為程式碼設計上存在的缺陷,導致專案在iphone5s以下的裝置執行時會出現比較明顯的卡頓現象。雖然instrument足夠優秀,但筆者更希望在程式執行期間能及時獲取卡頓資訊,因此開始動手自己的卡頓檢測方案。

獲取棧上下文

任何監控體系在監控到目標事件發生時,獲取執行緒的呼叫棧上下文是必須的,問題在於如何掛起當前執行緒並且獲取執行緒資訊。好在網上有大神分享了足夠多的資料供筆者查閱,讓筆者可以站在巨人的肩膀上來完成這部分業務。

demo中獲取呼叫棧程式碼重寫自BSBacktraceLogger,在使用之前建議能結合下方的參考資料和原始碼一起閱覽,知其然知其所以然。棧是一種後進先出(LIFO)的資料結構,對於一個執行緒來說,其呼叫棧的結構如下:
iOS監控:卡頓檢測

呼叫棧上每一個單位被稱作棧幀(stack frame),每一個棧幀由函式引數返回地址以及棧幀中的變數組成,其中Frame Pointer指向記憶體儲存了上一棧幀的地址資訊。換句話說,只要能獲取到棧頂的Frame Pointer就能遞迴遍歷整個棧上的幀,遍歷棧幀的核心程式碼如下:

從棧幀中我們只能獲取到呼叫函式的地址資訊,為了輸出上下文資料,我們還需要根據地址進行符號化,即找到地址所在的記憶體映象,然後定位該映象中的符號表,最後從符號表中匹配地址對應的符號輸出。
iOS監控:卡頓檢測

符號化過程中包括不限於以下的資料結構:

Dl_info儲存了包括路徑名、映象起始地址、符號地址和符號名等資訊

提供了符號表的偏移量,以及元素個數,還有字串表的偏移和其長度。更多堆疊的資料可以參考文末最後三個連結學習。符號化的核心函式lxd_dladdr如下:

整個符號化過程可以用下面的圖表示
iOS監控:卡頓檢測

關於RunLoop

RunLoop是一個重複接收著埠訊號和事件源的死迴圈,它不斷的喚醒沉睡,主執行緒的RunLoop在應用跑起來的時候就自動啟動,RunLoop的執行流程由下圖表示:
iOS監控:卡頓檢測

CFRunLoop.c中,可以看到RunLoop的執行程式碼大致如下:

通過原始碼不難發現RunLoop處理事件的時間主要出在兩個階段:

  • kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之間
  • kCFRunLoopAfterWaiting之後

監控RunLoop狀態檢測超時

通過RunLoop的原始碼我們已經知道了主執行緒處理事件的時間,那麼如何檢測應用是否發生了卡頓呢?為了找到合理的處理方案,筆者先監聽RunLoop的狀態並且輸出:

執行之後輸出的結果是滾動引發的Sources事件總是被快速的執行完成,然後進入到kCFRunLoopBeforeWaiting狀態下。假如在滾動過程中發生了卡頓現象,那麼RunLoop必然會保持kCFRunLoopAfterWaiting或者kCFRunLoopBeforeSources這兩個狀態之一。
iOS監控:卡頓檢測

為了實現卡頓的檢測,首先需要註冊RunLoop的監聽回撥,儲存RunLoop狀態;其次,通過建立子執行緒迴圈監聽主執行緒RunLoop的狀態來檢測是否存在停留卡頓現象: 收到Sources相關的事件時,將超時闕值時間內分割成多個時間片段,重複去獲取當前RunLoop的狀態。如果多次處在處理事件的狀態下,那麼可以視作發生了卡頓現象

標記位檢測執行緒超時

與UI卡頓不同的事,事件處理往往是處在kCFRunLoopBeforeWaiting的狀態下收到了Sources事件源,最開始筆者嘗試同樣以多個時間片段查詢的方式處理。但是由於主執行緒的RunLoop在閒置時基本處於Before Waiting狀態,這就導致了即便沒有發生任何卡頓,這種檢測方式也總能認定主執行緒處在卡頓狀態。

就在這時候寒神(南梔傾寒)推薦給我一套Swift的卡頓檢測第三方ANREye,這套卡頓監控方案大致思路為:建立一個子執行緒進行迴圈檢測,每次檢測時設定標記位為YES,然後派發任務到主執行緒中將標記位設定為NO。接著子執行緒沉睡超時闕值時長,判斷標誌位是否成功設定成NO。如果沒有說明主執行緒發生了卡頓,無法處理派發任務:
iOS監控:卡頓檢測

事後發現在特定情況下,這種檢測方式會出錯:當主執行緒被async大量的執行任務時,每個任務執行時間小於卡頓時間闕值,即對操作無影響。這時候由於設定標誌位的async任務位置過於靠後,導致子執行緒沉睡後未能成功設定,造成卡頓誤報的現象。(ps:當然,實測結果是基本不可能發生這種現象)這套方案解決了上面監聽RunLoop的缺陷。結合這套方案,當主執行緒處在Before Waiting狀態的時候,通過派發任務到主執行緒來設定標記位的方式處理常態下的卡頓檢測:

尾言

多數開發者對於RunLoop可能並沒有進行實際的應用開發過,或者說即便了解RunLoop也只是處在理論的認知上。當然,也包括呼叫堆疊追溯的技術。本文旨在通過自身實現的卡頓監控程式碼來讓更多開發者去了解這些深層次的運用與實踐。

此外,上面兩種檢測方案可以兼併使用,甚至只使用後者進行主執行緒的卡頓檢測也是可以的,本文demo已經上傳:LXDAppFluecyMonitor

參考資料

深入瞭解RunLoop
移動端監控體系之技術原理
趣探 Mach-O:FishHook 解析
iOS中執行緒Call Stack的捕獲和解析1-2

相關文章