位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路

語言: CN / TW / HK

本文主要介紹Heimdallr對卡死、卡頓異常的監控原理,並結合長時間的業務沉澱發現的問題進行不斷迭代和優化,逐步實現全面、穩定、可靠的歷程。 作者:位元組跳動終端技術——白崑崙

前言

卡死、卡頓作為目前iOS App的重要效能指標,不僅影響著使用者體驗,更關係到使用者留存、DAU等重要產品資料。本文主要介紹Heimdallr對卡死、卡頓異常的監控原理,並結合長時間的業務沉澱發現的問題進行不斷迭代和優化,逐步實現全面、穩定、可靠的歷程。

一、什麼是卡死/卡頓?

卡頓,顧名思義就是在使用過程中出現了一段時間的阻塞,使得使用者在這一段時間內無法進行操作,螢幕上的內容也沒有任何的變化。Heimdallr在監控指標上,根據阻塞時間的長短進行了3個等級的劃分。

圖片

1、流暢性與丟幀:動畫、滑動列表不流暢,一般為十幾至幾十毫秒的級別

2、卡頓:短時間操作無反應,恢復後能繼續使用,從幾百毫秒至幾秒

3、卡死:長時間無反應,直至被系統殺死,通過線上收集資料,最少為5s

可以看到,根據嚴重性由小至大可將卡頓問題劃分為流暢性與丟幀、卡頓、卡死三個不同的等級。卡死的嚴重程度與Crash是相當的,甚至更為嚴重。因為卡死不僅僅造成了類似於崩潰的閃退,更使得使用者被迫等待了相當長的一段時間,更加損害使用者的體驗。由於監控方案上的差異,本文主要面向的是後兩者卡頓和卡死的監控。

二、卡死/卡頓的原因

iOS開發中,由於UIKit是非執行緒安全的,因此一切與UI相關的操作都必須放在主執行緒執行,系統會每16ms(1/60幀)將UI的變化重新繪製,渲染至螢幕上。如果UI重新整理的間隔能小於16ms,那麼使用者是不會感到卡頓的。但是如果在主執行緒進行了一些耗時的操作,阻礙了UI的重新整理,那麼就會產生卡頓,甚至是卡死。主執行緒對於任務的處理是基於Runloop機制,如下圖所示。Runloop支援外部註冊通知回撥,提供了

1、RunloopEntry

2、RunloopBeforeTimers

3、RunloopBeforeSources

4、RunloopBeforeWaiting

5、RunloopAfterWaiting

6、RunloopExit

6個時機的事件回撥,其流轉關係如下圖所示。Runloop在沒有任務需要處理的時候就會進入至休眠狀態,直至有訊號將其喚醒,其又會去處理新的任務。

圖片

在日常編碼中,UIEvent事件、Timer事件、dispatch主執行緒任務都是在Runloop的迴圈機制的驅動下完成的。一旦我們在主執行緒中的任何一個環節進行了一個耗時的操作,或者因為鎖的使用不當造成了與其它執行緒的死鎖,主執行緒就會因為無法執行Core - Animation的回撥而造成介面無法重新整理。而使用者的互動又依賴於UIEvent的傳遞和響應,該流程也必須在主執行緒中完成。所以說主執行緒的阻塞會導致UI和互動的雙雙阻塞,這也是導致卡死、卡頓的根本原因。

三、監控方案

既然問題的根本在於主執行緒Runloop的阻塞,那麼我們就要通過技術手段監測主執行緒Runloop的執行狀態。為了能夠實時獲取主執行緒Runloop的狀態,首先對主執行緒註冊上面提到的幾個事件回撥,在觸發事件回撥時,利用signal機制將其執行狀態傳遞給另一個正在監聽的子執行緒(後面稱之為監聽執行緒)。監聽執行緒對於訊號的處理可以是多樣的,它可以設定等待signal的超時時間,如果超過了設定的閾值,這說明主執行緒可能正在經歷阻塞。通過監聽執行緒,我們可以完整地瞭解到主執行緒Runloop迴圈的週期,目前處於哪個階段,耗時了多久等等。根據這些必要的資訊,就可以採取對應的策略進行異常的捕獲和處理,後面會單獨就卡頓、卡死分別進行說明。

目前大多數APM工具都是採用監聽Runloop的方式進行卡頓的捕獲,這也是效能、準確性表現最好的一種方案。由於RunloopBeforeTimers的和RunloopBeforeSources是緊鄰的兩個事件回撥,Heimdallr為了降低Runloop頻繁事件回撥造成的效能損失,去除了對RunloopBeforeTimers的監聽。

  1. 卡頓(ANR)

卡頓監控的特點在於主執行緒的阻塞是暫時的、能夠恢復的,因此我們要獲取卡頓持續的時間,用來評估卡頓問題的嚴重性。我們預先設定一個卡頓時間的閾值T,當主執行緒阻塞的時間超過該閾值,則會觸發全執行緒的抓棧,獲取卡頓場景的堆疊資訊。此後監聽執行緒繼續等待主執行緒直至主執行緒恢復,並計算卡頓的總時間,整合之前獲取的堆疊資訊,上報卡頓異常。需要說明的是,如果在抓棧之後主執行緒無法恢復,那麼該異常不是卡頓,應交由卡死模組處理。

圖片

  1. 卡死(WatchDog)

與卡頓不同,卡死的阻塞是更長的,而且是無法恢復的。iOS系統會對App的主執行緒進行類似的監控,一旦發現了阻塞的情況,持續時間大於當前系統內允許的閾值(不同iOS版本和機型不同),就會強制殺死當前App程序,這個操作是沒有任何通知的。因此我們需要做的就是在系統發現卡死並強殺之前,獲取堆疊,並儘可能的評估出卡死持續的時間。

預先設定一個卡死的閾值T(預設是8s),這個閾值可以是相對保守的,並不是說超過了這個閾值就一定會被判定為卡死。在超過卡死閾值T的時候,獲取全執行緒的堆疊,並儲存至本地檔案中。之後每隔一段時間(取樣間隔,預設是1s),會進行一次取樣。取樣的目的不是為了獲取新的堆疊,而是為了更新卡死持續的時間,將該資訊儲存至本地檔案中。因此,取樣的間隔越小逼近真實卡死時間按越精確。直至到某一個時間節點,系統把App殺死。當App下一次啟動時,卡死模組會根據上一次啟動中保留的本地檔案資訊,還原出卡死的堆疊、持續時間等資訊,並上報卡死異常。

需要說明的是,很多人認為卡死一定是因為死鎖、死迴圈這樣的場景,導致程式永遠也無法完成導致的。其實不然,在很多場景下,一個或多個耗時的操作,只要其耗時超過了系統的允許閾值,都會觸發卡死。當應用啟動過程中,沒有在限定時間內完成初始化工作也會被系統殺死。所以,某些卡死可能是多個場景的不合理一起導致的,這也給卡死的問題定位提出了更高的要求。

圖片

四、問題與優化

理想是豐滿的,現實是骨感的。看似”無懈可擊“的監控方案,在線上卻暴露出不同程度的問題。

  1. 卡頓監控優化

在卡頓監控中,我們認為超過了卡頓閾值時獲取的堆疊一定是一個卡頓的場景,其實不然。在一些時候,獲取的堆疊可能是他人的”背鍋俠“。我們來看下面這個case。導致主執行緒卡頓的是4這個耗時操作,但是當我們設定閾值超時時,獲取的堆疊卻是沒有任何效能問題的5。因此如果使用這種方式來進行卡頓的監控,一定會存在誤報。而根據概率來講,雖然上報的5是一個誤報,但就線上的上報量來講,4的數量一定是要大於5的。因此上報量級大的堆疊才應該是真正的耗時操作,是需要我們專注去解決的,而那些量級較小的堆疊則可能是誤報。 

image.png

那麼是否能夠通過一些技術手段,在控制性能開銷的情況下,對卡頓場景捕捉的更加準確呢?一個比較好的思路就是取樣策略。如下圖,我們在原有的”常規模式“的基礎上增加了”取樣模式“。需要額外定義取樣間隔、取樣閾值。我們把卡頓閾值的等待過程,劃分為以取樣間隔為單位的粒度更細的時間節點。在每個時間節點進行主執行緒取樣,對主執行緒進行堆疊的提取。由於僅對主執行緒進行堆疊提取,所以耗時較全執行緒抓棧要小很多。

圖片

獲取了主執行緒堆疊後,通過提取頂層第一個自身呼叫來進行堆疊的聚合。如果某一個相同堆疊持續的時間超過了設定的取樣閾值,例如圖中的4,重複了3次,那麼就會判定該場景一定是一個卡頓場景。那麼此時就會進行全執行緒抓棧,而後面的卡頓閾值觸發時則不再抓棧。

結合主執行緒取樣,我們可以更加精準的以函式級別監控卡頓場景,但是也需要付出取樣帶來的額外效能開銷。為了將取樣的開銷降至最低,避免線上對低端裝置造成二次效能劣化,卡頓監控支援取樣功能的退火策略。當某一個卡頓場景被多次捕獲時,為了避免再次將其捕獲,造成不必要的效能浪費,會逐步增加取樣間隔,直至將”取樣模式“退化成”常規模式“。

圖片

在Slardar平臺配置並開啟取樣功能後,可以通過sample_flag來過濾通過取樣超時獲取的卡頓異常。通過此方式獲取的堆疊,大概率為卡頓場景,可以更加有針對性的去分析和解決。

  1. 卡死監控優化

相比卡頓,卡死的誤報大多發生在後臺(目前Heimdallr提供後臺卡死過濾,如果對後臺卡死不關心的業務方可以自行開啟)。因為後臺場景的限制,當前App的執行緒優先順序更低,而且隨時存在被系統掛起的可能,這給我們進行卡死時間的判定帶來了很多問題。

圖片

上面的Case描述的是一個卡死的誤報場景,因為在後臺的原因執行緒的優先順序較低,因此1、2、3任務執行的時間要比前臺更久,更加容易超過我們的卡死閾值。而後,因為iOS系統的策略問題,後臺應用被掛起(suspend),直至某一個時間點因為記憶體緊張,將整個應用殺死。但請注意,這個流程屬於App正常的生命週期範疇,並不是WatchDog。而按照我們之前的策略,這將會被判定為卡死。由於我們無法監聽到suspend事件,所以這種場景目前還無法排除誤報。

圖片

還有一種誤觸發卡死的case是,suspend發生在8s閾值前,在長時間的掛起後,應用被resume,此時8s的超時被觸發。但是實際上,我們的App只有在8s中的很少一部分時間在running,大部分時間都是被掛起,所以不應該觸發卡死判定。歸根結底是卡死計時的準確性問題。

圖片

為了解決上面的問題,對計時策略進行了改進。相比於直接進行8s的等待,我們將時間細分為8個1s。如果在這段時間內App被掛起,等到恢復時也不會直接超過8s的閾值,而僅僅會造成最多1s的誤差。\

此外,上面也提到過,卡死有的時候可能是多個耗時場景累計導致的。為了能夠跟蹤主執行緒的變化,在抓棧之後的取樣階段,對主執行緒進行堆疊取樣,並將其一起上報。結合取樣中獲取的主執行緒堆疊,我們可以得到一個主執行緒堆疊變化的時間線,能夠更加準確的幫助定位問題所在。(時間線功能在Heimdallr 0.7.15之後支援)圖片

最後,我們發現部分卡死場景是由於OC Runtime Lock導致的(大概率是dyldOC Runtime Lock造成的死鎖)。一旦發生這種型別的卡死,其它所有執行緒的OC程式碼都會因此而阻塞,當然也包括監聽執行緒,卡死監控此時就無法捕獲這個異常。為了能夠覆蓋所有場景,我們把卡死、卡頓模組的所有邏輯進行了C/C++重構,解除了對OC呼叫的依賴,並且效能相比與OC實現進一步得到提升。

結語

HeimdallrANRWatchDog模組經過一段時間的迭代與優化,達到了一個全面、穩定、可靠的狀態。這期間的一些優化思路借鑑了一些開源的APM框架,並結合使用方的實際需求進行不斷改進。感謝所有使用方的反饋,幫助我們不斷完善我們的功能與體驗。後續我們會繼續針對Watchdog場景增加防卡死功能,幫助接入方能夠在無侵入式的情況下,解決通用場景的卡死問題。


🔥 火山引擎 APMPlus 應用效能監控是火山引擎應用開發套件 MARS 下的效能監控產品。我們通過先進的資料採集與監控技術,為企業提供全鏈路的應用效能監控服務,助力企業提升異常問題排查與解決的效率。目前我們面向中小企業特別推出「APMPlus 應用效能監控企業助力行動」,為中小企業提供應用效能監控免費資源包。現在申請,有機會獲得60天免費效能監控服務,最高可享6000萬條事件量。

👉 點選這裡,立即申請