移動端性能監控方案Hertz,字節跳動社招面試記錄

不加班的程序猿 2021-09-18 07:02:55 阅读数:757

性能 方案 hertz

目前主流移動設備均采用雙緩存+垂直同步的顯示技術。大概原理是顯示系統有兩個緩沖區,GPU會預先渲染好一幀放入一個緩沖區內,讓視頻控制器讀取,當下一幀渲染好後,GPU會直接將視頻控制器的指針指向第二個容器。這裏,GPU會等待顯示器的VSync(即垂直同步)信號發出後,才進行新的一幀渲染和緩沖區更新。

大多數手機的屏幕刷新頻率是60HZ,如果在 1000/60=16.67ms 內沒有將這一幀的任務執行完畢,就會發生丟幀現象,這便是用戶感受到卡頓的原因。這一幀的繪制任務包括CPU的工作和GPU的工作兩部分,CPU負責計算顯示的內容,例如視圖創建、布局計算、圖片解碼、文本繪制等等,隨後CPU將計算好的內容提交給GPU,由GPU進行變換、合成、渲染。

除了UI繪制外,系統事件、輸入事件、程序回調服務、以及我們插入的其它代碼也都在主線程中執行,那麼一旦在主線程裏添加了操作複雜的代碼,這些代碼就有可能阻礙主線程去響應點擊、滑動事件,以及阻礙主線程的UI繪制操作,這就是造成卡頓的最常見原因。

在了解了屏幕繪制原理和卡頓形成的原因後,很容易想到通過檢測FPS就可以知道App是否發生了卡頓,也能够通過一段連續的FPS幀數計算丟幀率來衡量當前頁面繪制的質量。然而實踐發現FPS的刷新頻率非常快,並且容易發生抖動,因此直接通過比較通過FPS來偵測卡頓是比較困難的。而檢測主線程消息循環執行的時間就要容易的多了,這也是業內常用的一種檢測卡頓的方法。因此,Hertz在實踐中采用的就是檢測主線程每次執行消息循環的時間,當這一時間大於閾值時,就記為發生一次卡頓。

移動端性能監控方案Hertz,字節跳動社招面試記錄_Android

在實踐中我們發現,有的卡頓連續性耗時較長,例如打開新頁面時的卡頓;而有的卡頓連續性耗時相對較短但頻次較快,例如列錶滑動時的卡頓。因此,我們采用了“N次卡頓超過閾值T”的判定策略,即一個時間段內卡頓的次數累計大於N時才觸發采集和上報:例如卡頓閾值T=2000ms、卡頓次數N=1,可以判定為單次耗時較長的卡頓;而卡頓閾值T=300ms、卡頓次數N=5,可以判定為頻次較快的卡頓。

Runnable loopRunnable = new Runnable() {
@Override
public void run() {
if (mStartedDetecting && !isCatched) {
nowLaggyCount++;
if (nowLaggyCount >= N) {
blockHandler.onBlockEvent();
isCatched = true;
...
}
}
}
};
public void onMainLoopFinish(){
if(isCatched){
blockHandler.onBlockFinishEvent(loopStartTime,loopEndTime);
}
resetStatus();
...
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

當檢測到卡頓後,如何定比特到造成卡頓的問題呢?如果能抓取到卡頓發生時程序的調用堆棧和運行日志,是不是很酷?的確,通過抓取堆棧可以非常有效地幫我們定比特到造成卡頓的“問題代碼”。

在實踐中我們發現抓取堆棧有兩個需要注意的問題。

第一個問題是堆棧抓取的時機。抓取堆棧的時機必須是在卡頓發生當時,而不是之後,否則不能准確抓到造成卡頓的代碼,因此在子線程中當卡頓還沒有結束時,我們就會抓取堆棧。

第二個問題是堆棧如何歸類,卡頓堆棧的歸類和Crash堆棧不同,以最內層代碼歸類顯然是不合適的,因為外層不同的業務邏輯代碼在最內層的調用堆棧有可能是相同的。以最外層代碼歸類也是不合適的,因為最外層代碼有可能是業務邏輯代碼,也有可能是系統調用。

目前Hertz的做法是按照最內層歸類的原則,並匹配一些簡單的規則,以命中規則的類名來歸類。

擴展性和易用性

Hertz非常重視SDK的可擴展性和易用性,在設計之初我們就做了很多考量。SDK的框架如下圖所示,整體上分為三層:最上層是接口層,提供極少量的對外暴露的方法,以及環境和配置參數等。第二層是業務層,包含了頁面測速、卡頓檢測和參數采集等所有的核心邏輯。第三層是數據適配層,將業務層產生的數據封裝為統一的數據結構,並通過適配器適配到不同的輸出通道上。

移動端性能監控方案Hertz,字節跳動社招面試記錄_程序員_02

設計上我們第一個考量就是接口的易用性,Hertz內置了三種運行模式:開發模式、測試模式和線上模式。開發者只需要指定一種模式,Hertz就可以開始工作了。各種模式預設了SDK運行所需要的參數,例如采樣頻率、卡頓閾值、上報通道開關等,而監控指標的采集、卡頓的偵測、頁面測速等邏輯都在內部自動執行。以Android為例,示例代碼如下:

final HertzConfiguration configuration = new HertzConfiguration.Builder(this)
.mode(HertzMode.HERTZ_MODE_DEBUG)
.appId(APP_ID)
.unionId(UNION_ID)
.build();
Hertz.getInstance().init(configuration);

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

設計上我們第二個考量是SDK的可擴展性。以數據適配層為例,目前內置了五種適配通道,可以將采集到的監控數據適配到不同的數據通道。根據選擇的工作模式不同,數據將被適配到服務端監控通道,生成測試報告,或者只在App本地輸出日志和提示。這種設計帶來的一個好處是,如果需要新增一種數據輸出通道,既可以在上層添加一個攔截器,也可以只改動SDK極少量的代碼來添加一個適配器。同樣的,性能采集模塊和頁面測速模塊的設計也遵循這種思路。

實際應用

美團外賣在接入Hertz後,初步具備了發現、定比特性能問題的能力,在開發期、測試期、線上期都對Hertz進行了實際驗證。

開發期應用

在開發期接入Hertz,相當於集成了一個離線的性能檢測工具,當檢測到异常時,Hertz將這些數據直接反饋給開發者,如下圖所示:

移動端性能監控方案Hertz,字節跳動社招面試記錄_Android_03

運行時采集的數據會輸出到日志中,而App的頁面上也會插入一個浮層來展示當前的FPS、CPU、內存等基本信息。如果檢測到卡頓發生,會彈出提示頁面並列出當前的執行堆棧。目前從卡頓檢測結果來看,大部分堆棧日志可以比較明顯的定比特到有問題的代碼,只要略微查看代碼和分析原因,這些問題都能很容易的優化。

下面是初始化複雜UI造成卡頓的例子:

android.content.res.StringBlock.nativeGetString(Native Method)
android.content.res.StringBlock.get(StringBlock.java:82)
android.content.res.XmlBlock$Parser.getName(XmlBlock.java:175)
android.view.LayoutInflater.inflate(LayoutInflater.java:470)
android.view.LayoutInflater.inflate(LayoutInflater.java:420)
android.view.LayoutInflater.inflate(LayoutInflater.java:371)
com.sankuai.meituan.takeoutnew.controller.ui.PoiListAdapterController.getView(PoiListAdapterController.java:77)
com.sankuai.meituan.takeoutnew.adapter.PoiListAdapter.getView(PoiListAdapter.java:26)
android.widget.HeaderViewListAdapter.getView(HeaderViewListAdapter.java:220)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

下面是使用Gson反向解析字符串時造成卡頓的例子:

com.google.gson.Gson.toJson(Gson.java:519)
com.meituan.android.common.locate.util.GoogleJsonWrapper $MyGson.toJson(GoogleJsonWrapper.java:236)
com.sankuai.meituan.location.collector.CollectorJson $MyGson.toJson(CollectorJson.java:216)
com.sankuai.meituan.location.collector.CollectorFilter.saveCurrentData(CollectorFilter.java:67)
com.sankuai.meituan.location.collector.CollectorFilter.init(CollectorFilter.java:33)
com.sankuai.meituan.location.collector.CollectorFilter.<init>(CollectorFilter.java:27)
com.sankuai.meituan.location.collector.CollectorMsgHandler.recordGps(CollectorMsgHandler.java:134)
com.sankuai.meituan.location.collector.CollectorMsgHandler.getNewLocation(CollectorMsgHandler.java:81)
com.meituan.android.common.locate.LocatorMsgHandler$1.handleMessage(LocatorMsgHandler.java:29)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

下面是主線程讀寫數據庫造成卡頓的例子:

android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:782)
android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
de.greenrobot.dao.AbstractDao.executeInsert(AbstractDao.java:306)
de.greenrobot.dao.AbstractDao.insert(AbstractDao.java:276)
com.sankuai.meituan.takeoutnew.db.dao.BaseAbstractDao.insert(BaseAbstractDao.java:25)
com.sankuai.meituan.takeoutnew.log.LogDataUtil.insertIntoDb(LogDataUtil.java:243)
com.sankuai.meituan.takeoutnew.log.LogDataUtil.saveLogInfo(LogDataUtil.java:221)
com.sankuai.meituan.takeoutnew.log.LogDataUtil.saveLog(LogDataUtil.java:116)
com.sankuai.meituan.takeoutnew.log.LogDataUtil.saveLogInfo(LogDataUtil.java:112)
com.sankuai.meituan.takeoutnew.ui.page.main.order.OrderListFragment.onPageShown(OrderListFragment.java:306)
com.sankuai.meituan.takeoutnew.ui.page.main.order.OrderListFragment.init(OrderListFragment.java:151)
com.sankuai.meituan.takeoutnew.ui.page.main.order.OrderListFragment.onCreateView(OrderListFragment.java:81)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

從上報的具體問題來看,大部分日志可以比較明顯的定比特到有問題的代碼,只要略微查看代碼和分析原因,這些問題都能很容易優化。

測試期應用

傳統的性能測試大多依賴於第三方工具,產生的數據和開發實測的數據有較大出入,此外,這些測試往往只給出一些指標的數據,而不能幫助開發者定比特到問題所在。我們在測試階段使用Hertz采集性能數據,測試手段可以是人工測試,也可以是自動化測試或者monkey測試。得到性能數據後,通過脚本處理後會發出一個簡單的測試報告。

移動端性能監控方案Hertz,字節跳動社招面試記錄_Android_04

當然這種形式的測試報告仍然需要手工來導出日志和執行脚本,未來我們會在此基礎上開發一套自動化的測試工具。

上線期應用

對於卡頓檢測,除了在開發期和測試期Hertz能立即將問題反饋給開發者外,在灰度或線上運行時Hertz也會將數據上傳到服務端,目前上報通道是公司內部的CAT(已經開源,詳情請參考[深度剖析開源分布式監控CAT](

)一文)。可以看到堆棧的歸類和展示和我們熟悉的Crash監控非常類似,按照前面提到的歸類原則,卡頓堆棧按照發生的次數排列,並且可以按照版本、操作系統、設備過濾,比較符合開發者的使用習慣。

移動端性能監控方案Hertz,字節跳動社招面試記錄_程序員_05

對於流量的統計,我們每天會上報到服務端全網用戶的流量消耗數據,並輸出一個報錶,列出全網流量消耗Top100的用戶。如果發現异常,可以進一步根據後端日志和客戶端診斷日志來排查具體是哪個網絡請求導致的流量异常。

移動端性能監控方案Hertz,字節跳動社招面試記錄_程序員_06

對於頁面測速數據和FPS、CPU、內存等基礎指標,Hertz也會將數據上報到CAT,評測App整體的性能狀况。

移動端性能監控方案Hertz,字節跳動社招面試記錄_移動開發_07

總結

性能優化是每一個成熟的App都必須認真對待的話題,而性能優化的痛點往往在於不能及時發現問題,或者發現了問題卻不能定比特問題。美團外賣以監控數據指導性能優化的思路,在實踐中開發和完善了App性能監控方案Hertz,並且在性能數據的監控和應用方面做了一些探索和驗證。

目前Hertz的監控指標包括了FPS、CPU使用率、內存占用、卡頓、頁面加載時間、網絡請求流量等,而耗電量、App冷啟動,以及Exception等監控後續會逐步加入到Hertz的監控目標中去。性能監控的指標在未來可能會複用多個現有工具,並且在此基礎上逐步完善

Hertz的卡頓偵測和堆棧抓取能够非常有效地幫助開發者定比特性能問題,但是目前的卡頓偵測策略還有很多優化的空間。例如是否可以根據設備不同設定不同的閾值,以及在App運行的不同時期設置不同的策略。而對於堆棧的歸類,目前的規則只是簡單地匹配類名前綴,如何更精准、更合理的分類也是我們未來要更多考慮的問題。當然,這些優化還需要更多的數據樣本做支撐。

建立可視化的、友好的性能測試工具也同樣非常重要,例如一個可實時查看,也可翻閱曆史報告的Web頁面。同時,Hertz在設計上可以很容易的和自動化測試手段相結合,或者在集成階段自動生成測試報告,然而在這方面我們才僅僅做了一些初步的嘗試。當我們具備了准確采集性能數據的能力之後,如何更好地應用到包括測試環節在內的整個開發流程中,仍然需要長期的探索和實踐

本文主要介紹美團外賣在Hertz的實踐過程中總結的一些思路和實現手段,而圍繞App性能監控還有很多有趣的,和更深入的主題並沒有涉及。例如如何平衡性能監控工具和工具本身所帶來的性能問題,性能優化的具體技巧和手段,以及對性能數據做進一步分析從而建立起异常設備的監控體系等等。未來我們也將在這些問題上做進一步探索、實踐和分享。

參考文獻

  1. [BlockCanary](

).
2. [Leakcanary](

).
3. [Watchdog](

).
4. [iOS-System-Services](

).
5. guoling, [微信iOS卡頓監控系統](

).

回答“思考題”、發現文章有錯誤、對內容有疑問,都可以來微信公眾號(美團點評技術團隊)後臺給我們留言。我們每周會挑選出一比特“優秀回答者”,贈送一份精美的小禮品。快來掃碼關注我們吧!

移動端性能監控方案Hertz,字節跳動社招面試記錄_移動開發_08

最後

針對於上面的問題,我總結出了互聯網公司Android程序員面試涉及到的絕大部分面試題及答案,並整理做成了文檔,以及系統的進階學習視頻資料。
(包括Java在Android開發中應用、APP框架知識體系、高級UI、全方比特性能調優,NDK開發,音視頻技術,人工智能技術,跨平臺技術等技術資料),希望能幫助到你面試前的複習,且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。

 CodeChina開源項目:《Android學習筆記總結+移動架構視頻+大廠面試真題+項目實戰源碼》

移動端性能監控方案Hertz,字節跳動社招面試記錄_移動開發_09

版权声明:本文为[不加班的程序猿]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210918070254673j.html