如何檢測 iOS app 卡頓導(dǎo)致的系統(tǒng)強殺

在之前的文章中提到過,現(xiàn)有市面上的 iPhone 老設(shè)備(特指 iPhone 6s 之前的設(shè)備)占有率高達 40%,iOS app 卡頓的發(fā)生率發(fā)生概率也很高??D里有一類卡頓又尤其嚴重:主線程長期不響應(yīng)而導(dǎo)致的系統(tǒng) Watchdog 強殺。

現(xiàn)在很多 iOS 線上 App 都集成了卡頓檢測工具,原理多是基于 runloop 的各個事件回調(diào),這類工具可以檢測主線程是否在某個 threshold(比如 2 秒) 內(nèi)是否處于可反應(yīng)狀態(tài),比如重復(fù)進入某個 runloop 即可認為主線程沒有被卡住。我們姑且稱這類工具為 AppWatchdog,但對于嚴重的主線程卡頓,比如超過 10 秒會被系統(tǒng)強殺,我們稱這種系統(tǒng)機制為 iOSWatchdog。為了方便表述,我們進一步將 AppWatchdog 偵測到的卡頓我們稱為 stall,iOSWatchdog 強殺的卡頓我們稱為 hard stall。

AppWatchdog 偵測到的 stall 很好分析,我們只需要在發(fā)生 stall 時,在另外一個線程將主線程的 call stack 記錄下來上報即可,而后對癥下藥在下個版本修復(fù)。但對于 hard stall,準確檢測很難,原因是系統(tǒng)強殺時并沒有任何信號提醒,也不會像 crash 一樣生成一個 report,現(xiàn)在主流 App 是怎么做的呢?

我們先從 Facebook 曾經(jīng)公開發(fā)布過的一篇相關(guān)文章說起:Reducing FOOMs in the Facebook iOS app。

這篇文章系統(tǒng)化的提出了 iOS App 冷啟動分析方法,比如 App 升級,用戶強殺,App crash,系統(tǒng)升級,后臺內(nèi)存不夠(BOOM),前臺內(nèi)存不夠(FOOM)等,類似一個 pipeline,一個個分析下來,最后剩下的就是 FOOM,即 app 在前臺由于消耗過多內(nèi)存而被系統(tǒng)強殺。

近兩年大家開始意識到這個 pipeline 漏分析了一個重要的冷啟動原因:hard stall,而且事實證明 hard stall 出現(xiàn)的概率還不低??赡艽蠹乙矝]意識到 hard stall 會這么容易出現(xiàn)在線上 App 中。

在繼續(xù)分析之前,我們再進一步明確下 hard stall 的定義,一般我們指系統(tǒng)強殺為 hard stall,但一般用戶會在 App 卡頓長達 10 秒之久時依舊等待嗎?我比較懷疑,我認為相當一部分用戶會提前手動殺掉 App,還有一小部分用戶會直接放棄當前 App 進入后臺。我覺得我們可以將這類導(dǎo)致用戶中斷當前任務(wù)的卡頓都稱為 hard stall,雖然不是系統(tǒng)強殺,但在嚴重性質(zhì)上相差無幾。

微信客戶端團隊曾經(jīng)分享過一篇類似主題的文章:iOS微信內(nèi)存監(jiān)控 ,其中有一段相關(guān)表述:

前臺卡死引起系統(tǒng)watchdog強殺

也就是常見的0x8badf00d,通常原因是前臺線程過多,死鎖,或CPU使用率持續(xù)過高等,這類強殺無法被App捕獲。為此我們結(jié)合了已有卡頓系統(tǒng),當前臺運行最后一刻有捕獲到卡頓,我們認為這次啟動是被watchdog強殺。同時我們從FOOM劃分出新的重啟原因叫“APP前臺卡死導(dǎo)致重啟”,列入重點關(guān)注。

所以微信客戶端的做法是檢查 stall 的時間戳和冷啟動的時間戳,如果二者比較接近,則認為是 hard stall。這種做法應(yīng)該比較簡單有效,但無法檢測用戶放棄當前任務(wù)的 hard stall 場景,而”‘相近“的 threshold 取多少秒也難以把握。FB 內(nèi)部也有一套機制來區(qū)分 stall 與 hard stall,但我個人感覺也不是十分準確,最近在思考如何改進這一檢測機制,現(xiàn)在大致有個思路和大家分享,等下半年抽空實踐下。

如何檢測

用一句話來概括這個思路就是:如果 stall 導(dǎo)致 runloop 無法從當前任務(wù)中恢復(fù),則認之為 hard stall。

我們知道,主線程的 runloop 用來處理各種任務(wù),每一次 loop 會觸發(fā)幾種不同類型的回調(diào):

kCFRunLoopBeforeTimers 
kCFRunLoopBeforeSources 
kCFRunLoopBeforeWaiting 
kCFRunLoopAfterWaiting 

某個回調(diào)可能會觸發(fā)我們客戶端里的某個耗時任務(wù),一般一個 loop 里會觸發(fā)多個任務(wù),比如 job1,job2,job3,執(zhí)行順序及耗時如下:

job1(10ms) --> job2(1500ms)-->job3(15000ms) --> 一小時之后冷啟動

很明顯,job1 是安全的,job2 會觸發(fā) stall,并被我們的 AppWatchdog 工具檢測上報(假設(shè) threshold 為 2s),job3 會首先被 AppWatchdog 檢測,而后由于長期阻塞主線程,被系統(tǒng) watchdog 強殺,是真正的 hard stall。那么當我們在后臺看到 job2 和 job3 的上報日志時,怎么判斷那個才是 hard stall 呢?顯然我們無法記錄每一個任務(wù)的執(zhí)行時間,所以并不知道 job2 和 job3 哪個更嚴重,或者說 job3 是不是足夠嚴重。

回到上面對于思路的概括,我們就看 runloop 能否從 stall 當中恢復(fù)出來,我們可以在 runloop 每次進入不同事件的時候,如果上次發(fā)生過 stall,我們就記錄一個新的時間戳 activeTs,來表示 runloop 在未來某個時間點從 stall 中恢復(fù)了,然后再在下次冷啟動的時候做如下判斷:

if (report.stallTs < activeTs) {
  report.isHardStall = true;
} else {
  report.isHardStall = false;
}

所以上面的時間序列會變成:

job1(10ms) --> job2(1500ms)-->activeTs-->job3(15000ms) --> 一小時之后冷啟動

很顯然,job2 執(zhí)行之后,我們記錄一個 runloop 活躍的時間戳,那么表明 job2 是常規(guī) stall,而 job3 由于耗時過長導(dǎo)致系統(tǒng)強殺,runloop 沒有機會進入下一次 loop,則沒有記錄下最新的 activeTs,所以,依據(jù)上面的條件判斷,可以輕易的檢測出 job2 為常規(guī) stall,而 job3 為 hard stall。

這種判斷機制更有針對性,所以即使 hard stall 之后用戶放棄當前任務(wù),過很長時間之后再次啟動 App,我們依然能夠判斷出哪個 stall(call stack)是真正的 hard stall。

一些細節(jié)

說完大致思路,看著好像并不怎么復(fù)雜,但魔鬼藏在細節(jié)里,有哪些需要注意的細節(jié)呢?暫時想到的有:

  • stall 檢測工具是為了檢測 stall,所以本身應(yīng)該輕量,盡量避免費時的任務(wù)或者是 disk I/O,而記錄 activeTs 必然會有一次額外的磁盤寫操作,我們應(yīng)該做到一次 App 啟動最多只記錄一次 activeTs 到磁盤里,而且只發(fā)生在有 stall 的情況下。
  • 為了效率起見,在下次冷啟動的時候,我們應(yīng)該只處理上次啟動留下的 stall reports,也能夠避免記錄過多的 activeTs。為此,我們需要引入一個 Session 的概念,即每一次啟動對應(yīng)一次 Session,每個 Session 只處理上一個 Session 的 stall reports,我們可以將一個連續(xù)自增長的 Session ID 寫入 stall report 里,這樣就知道每一個 stall 對應(yīng)那一次啟動,甚至可以將 Session ID 寫入 stall report 的文件名,方便過濾,既高效有簡潔。
  • 如果 runloop 在執(zhí)行完某個任務(wù)進入休眠,獲得 kCFRunLoopBeforeWaiting 回調(diào),此時我們也需要記錄 activeTs,因為能夠進入休眠表明非 hard stall。

最后用圖再描述下具體思路:

hardstall00.png

等待真正去實現(xiàn)的時候,估計還有不少細節(jié)需要處理,后面做好了再更新一篇文章。

全文完。

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

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

  • 最近在調(diào)研 iOS app 中存在的各種卡頓現(xiàn)象以及解決方法。 iOS App 出現(xiàn)卡頓(stall)的概率可能超...
    MrPeak閱讀 4,369評論 1 52
  • 概述 RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣。盡管在平時多...
    sumrain_cloud閱讀 1,004評論 0 5
  • 概述 RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣。盡管在平時多...
    陽明AI閱讀 1,151評論 0 17
  • 說明iOS中的RunLoop使用場景1.保持線程的存活,而不是線性的執(zhí)行完任務(wù)就退出了<1>不開啟RunLoop的...
    野生塔塔醬閱讀 6,918評論 15 109
  • 時光飛逝,手機這個品種在世界上已經(jīng)有40多個年頭了。在這個翻天覆地的變化中,如果一定要給手機做一些分門別類,那么商...
    不許瞎搞閱讀 459評論 0 0

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