iOS app 卡頓導(dǎo)致的系統(tǒng)強(qiáng)殺

卡頓里有一類卡頓又尤其嚴(yán)重:主線程長期不響應(yīng)而導(dǎo)致的系統(tǒng) Watchdog 強(qiáng)殺。

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

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

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

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

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

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

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

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

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

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

如何檢測

用一句話來概括這個思路就是:如果 stall 導(dǎo)致 runloop 無法從當(dāng)前任務(wù)中恢復(fù),則認(rèn)之為 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 強(qiáng)殺,是真正的 hard stall。那么當(dāng)我們在后臺看到 job2 和 job3 的上報日志時,怎么判斷那個才是 hard stall 呢?顯然我們無法記錄每一個任務(wù)的執(zhí)行時間,所以并不知道 job2 和 job3 哪個更嚴(yán)重,或者說 job3 是不是足夠嚴(yán)重。

回到上面對于思路的概括,我們就看 runloop 能否從 stall 當(dāng)中恢復(fù)出來,我們可以在 runloop 每次進(jìn)入不同事件的時候,如果上次發(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)強(qiáng)殺,runloop 沒有機(jī)會進(jìn)入下一次 loop,則沒有記錄下最新的 activeTs,所以,依據(jù)上面的條件判斷,可以輕易的檢測出 job2 為常規(guī) stall,而 job3 為 hard stall。

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

一些細(xì)節(jié)

說完大致思路,看著好像并不怎么復(fù)雜,但魔鬼藏在細(xì)節(jié)里,有哪些需要注意的細(xì)節(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ù)進(jìn)入休眠,獲得 kCFRunLoopBeforeWaiting 回調(diào),此時我們也需要記錄 activeTs,因為能夠進(jìn)入休眠表明非 hard stall。

?著作權(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)容

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