想讓安卓app不再卡頓?看這篇文章就夠了
本文由likunhuang發表於雲+社區專欄
實現背景
應用的使用流暢度,是衡量用戶體驗的重要標準之一。Android 由於機型配置和系統的不同,項目復雜App場景豐富,代碼多人參與叠代歷史較久,代碼可能會存在很多UI線程耗時的操作,實際測試時候也會偶爾發現某些業務場景發生卡頓的現象,用戶也經常反饋和投訴App使用遇到卡頓。因此,我們越來越關註和提升用戶體驗的流暢度問題。
已有方案
在這之前,我們將反饋的常見卡頓場景,或測試過程中常見的測試場景使用UI自動化來重復操作,用adb系統工具觀察App的卡頓數據情況,試圖重現場景來定位問題。
常用的方式是使用adb SurfaceFlinger服務和adb gfxinfo功能,在自動化操作app的過程中,使用adb獲取數據來監控app的流暢情況,發現出現出現卡頓的時間段,尋找出現卡頓的場景和操作。
方式1:adb shell dumpsys SurfaceFlinger
使用‘adb shell dumpsys SurfaceFlinger’命令即可獲取最近127幀的數據,通過定期執行adb命令,獲取幀數來計算出幀率FPS。
優點:命令簡單,獲取方便,動態頁面下數據直觀顯示頁面的流暢度;
缺點:對於靜態頁面,無法感知它的卡頓情況。使用FPS在靜態頁面情況下,由於獲取數據不變,計算結果為0,無法有效地衡量靜態頁面卡頓程度;
通過外部adb命令取得的數據信息衡量app頁面卡頓情況的同時,app層面無法在運行時判斷是否卡頓,也就無法記錄下當時運行狀態和現場信息。
方式2:adb shell dumpsys gfxinfo
使用‘adb shell dumpsys gfxinfo’命令即可獲取最新128幀的繪制信息,詳細包括每一幀繪制的Draw,Process,Execute三個過程的耗時,如果這三個時間總和超過16.6ms即認為是發生了卡頓。
優點:命令簡單,獲取方便,不僅可以計算幀率,還可以觀察卡頓時每一幀的瓶頸處於哪個維度(onDraw,onProcess,onExecute);
缺點:同方式1擁有一樣的缺點,無法衡量靜態頁面下的卡頓程度;app層面依然無法在發生卡頓時獲取運行狀態和信息,導致跟進和重現困難。
已有的兩種方案比較適合衡量回歸卡頓問題的修復效果和判斷某些特定場景下是否有卡頓情況,然而,這樣的方式有幾個明顯的不足:
1、一般很難構造實際用戶卡頓的環境來重現;
2、這種方式操作起來比較麻煩,需編寫自動化用例,無法覆蓋大量的可疑場景,測試重現耗時耗人力;
3、無法衡量靜態頁面的卡頓情況;
4、出現卡頓的時候app無法及時獲取運行狀態和信息,開發定位困難。
全新方案
基於這樣的痛點,我們希望能使用一套有效的檢測機制,能夠覆蓋各種可能出現的卡頓場景,一旦發生卡頓,能幫助我們更方便地定位耗時卡頓發生的地方,記錄下具體的信息和堆棧,直接從代碼程度給到開發定位卡頓問題。我們設想的Android卡頓監控系統需要達到幾項基本功能:
1、如何有效地監控到App發生卡頓,同時在發生卡頓時正確記錄app的狀態,如堆棧信息,CPU占用,內存占用,IO使用情況等等;
2、統計到的卡頓信息上報到監控平臺,需要處理分析分類上報內容,並通過平臺Web直觀簡便地展示,供開發跟進處理。
如何從App層面監控卡頓?
我們的思路是,一般主線程過多的UI繪制、大量的IO操作或是大量的計算操作占用CPU,導致App界面卡頓。只要我們能在發生卡頓的時候,捕捉到主線程的堆棧信息和系統的資源使用信息,即可準確分析卡頓發生在什麽函數,資源占用情況如何。那麽問題就是如何有效檢測Android主線程的卡頓發生,目前業界兩種主流有效的app監控方式如下,在《Android卡頓監控方式實現》這篇文章中我將分別詳細闡述這兩者的特點和實現。
1、利用UI線程的Looper打印的日誌匹配;
2、使用Choreographer.FrameCallback
方式3: 利用UI線程的Looper打印的日誌匹配判斷是否卡頓
Android主線程更新UI。如果界面1秒鐘刷新少於60次,即FPS小於60,用戶就會產生卡頓感覺。簡單來說,Android使用消息機制進行UI更新,UI線程有個Looper,在其loop方法中會不斷取出message,調用其綁定的Handler在UI線程執行。如果在handler的dispatchMesaage方法裏有耗時操作,就會發生卡頓。
只要檢測msg.target.dispatchMessage(msg) 的執行時間,就能檢測到部分UI線程是否有耗時的操作,從而判斷是否發生了卡頓,並打印UI線程的堆棧信息。
優點:用戶使用app或者測試過程中都能從app層面來監控卡頓情況,一旦出現卡頓能記錄app狀態和信息, 只要dispatchMesaage執行耗時過大都會記錄下來,不再有前面兩種adb方式面臨的問題與不足。
缺點:需另開子線程獲取堆棧信息,會消耗少量系統資源。
方式4: 利用Choreographer.FrameCallback監控卡頓
我們知道, Android系統每隔16ms發出VSYNC信號,來通知界面進行重繪、渲染,每一次同步的周期為16.6ms,代表一幀的刷新頻率。SDK中包含了一個相關類,以及相關回調。理論上來說兩次回調的時間周期應該在16ms,如果超過了16ms我們則認為發生了卡頓,利用兩次回調間的時間周期來判斷是否發生卡頓(這個方案是Android 4.1 API 16以上才支持)。
這個方案的原理主要是通過Choreographer類設置它的FrameCallback函數,當每一幀被渲染時會觸發回調FrameCallback, FrameCallback回調void doFrame (long frameTimeNanos)函數。一次界面渲染會回調doFrame方法,如果兩次doFrame之間的間隔大於16.6ms說明發生了卡頓。
優點:不僅可用來從app層面來監控卡頓,同時可以實時計算幀率和掉幀數,實時監測App頁面的幀率數據,一旦發現幀率過低,可自動保存現場堆棧信息。
缺點:需另開子線程獲取堆棧信息,會消耗少量系統資源。
總結下上述四種方案的對比情況:
SurfaceFlinger | gfxinfo | Looper.loop | Choreographer.FrameCallback | |
---|---|---|---|---|
監控是否卡頓 | √ | √ | √ | √ |
支持靜態頁面卡頓檢測 | × | × | √ | √ |
支持計算幀率 | √ | √ | × | √ |
支持獲取App運行信息 | × | × | √ | √ |
實際項目使用中,我們一開始兩種監控方式都用上,上報的兩種方式收集到的卡頓信息我們分開處理,發現卡頓的監控效果基本相當。同一個卡頓發生時,兩種監控方式都能記錄下來。 由於Choreographer.FrameCallback的監控方式不僅用來監控卡頓,也方便用來計算實時幀率,因此我們現在只使用Choreographer.FrameCallback來監控app卡頓情況。
痛點1:如何保證捕獲卡頓堆棧的準確性?
細心的同學可以發現,我們通過上述兩種方案(Looper.loop和Choreographer.FrameCallback)可以判斷是當前主線程是否發生了卡頓,進而在計算發現卡頓後的時刻dump下了主線程的堆棧信息。實際上,通過一個子線程,監控主線程的活動情況,計算發現超過閾值後dump下主線程的堆棧,那麽生成的堆棧文件只是捕捉了一個時刻的現場快照。打個不太恰當的比方,相當於閉路電視監控只拍下了兇案發生後的慘狀,而並沒有錄下這個案件發生的過程,那麽作為警察的你只看到了結局,依然很難判斷案情和兇手。在實際的運用中,我們也發現這種方式下獲取到的堆棧情況,查看相關的代碼和函數,經常已經不是發生卡頓的代碼了。
如圖所示,主線程在T1~T2時間段內發生卡頓,上述方案中獲取卡頓堆棧的時機已經是T2時刻。實際卡頓可能是這段時間內某個函數的耗時過大導致卡頓,而不一定是T2時刻的問題,如此捕獲的卡頓信息就無法如實反應卡頓的現場。
我們看看在這之前微信iOS主線程卡頓監控系統是如何實現的捕獲堆棧。微信iOS的方案是起檢測線程每1秒檢查一次,如果檢測到主線程卡頓,就將所有線程的函數調用堆棧dump到內存中。本質上,微信iOS方案的計時起點是固定的,檢查次數也是固定的。如果任務1執行花費了較長的時間導致卡頓,但由於監控線程是隔1秒掃一次的,可能到了任務N才發現並dump下來堆棧,並不能抓到關鍵任務1的堆棧。這樣的情況的確是存在的,只不過現上監控量大走人海戰術,通過概率分布抓到卡頓點,但依然不是最佳的捕獲方案。
因此,擺在我們面前的是如何更加精準地獲取卡頓堆棧。為了卡頓堆棧的準確度,我們想要能獲取一段時間內的堆棧,而不是一個點的堆棧,如下圖:
我們采用高頻采集的方案來獲取一段卡頓時間內的多個堆棧,而不再是只有一個點的堆棧。這樣的方案的優點是保證了監控的完備性,整個卡頓過程的堆棧都得以采樣、收集和落地。
具體做法是在子線程監控的過程中,每一輪log輸出或是每一幀開始啟動monitor時,我們便已經開啟了高頻采樣收集主線程堆棧的工作了。當下一輪log或者下一幀結束monitor時,我們判斷是否發生卡頓(計算耗時是否超過閾值),來決定是否將內存中的這段堆棧集合落地到文件存儲。也就是說,每一次卡頓的發生,我們記錄了整個卡頓過程的多個高頻采樣堆棧。由此精確地記錄下整個兇案發生的詳細過程,供上報後分析處理(後文會闡述如何從一次卡頓中多個堆棧信息中提取出關鍵堆棧)。
采樣頻率與性能消耗
目前我們的策略是判斷一個卡頓是否發生的耗時閾值是80ms(5*16.6ms),當一個卡頓達80ms的耗時,采集1~2個堆棧基本可以定位到耗時的堆棧。因此采樣堆棧的頻率我們設為52ms(經驗值)。
當然,高頻采集堆棧的方案,必然會導致app性能上帶來的影響。為此,為了評估對App的性能影響,在上述默認設置的情況下,我們做一個簡單的測試實驗觀察。實驗方法:ViVoX9 上運行微信讀書App,使用卡頓監控與高頻采樣,和不使用卡頓監控的情況下,保持兩次的操作動作相同,分析性能差異,數據如下:
關閉監控 | 打開監控 | 對比情況(上漲) | ||
---|---|---|---|---|
CPU | 1.07% | 1.15% | 0.08% | |
Memory | Native Heap | 38794 | 38894 | 100 kB |
Dalvik Heap | 25889 | 26984 | 1095 kB | |
Dalvik Other | 2983 | 3099 | 116 kB | |
.so mmap | 38644 | 38744 | 100 kB |
沒有線程快照 | 加上線程快照 | |||
---|---|---|---|---|
性能指標 | 2.4.5.368.91225 | 2.4.8.376.91678 | 上漲 | |
CPU | CPU | 63 | 64 | 0.97% |
流量KB | Flow | 28624 | 28516 | |
內存KB | NativeHeap | 59438 | 60183 | 1.25% |
DalvikHeap | 7066 | 7109 | 0.61% | |
DalvikOther | 6965 | 6992 | 0.40% | |
Sommap | 22206 | 22164 | ||
日誌大小KB | file size | 294893 | 1561891 | 430% |
壓縮包大小KB | zip size | 15 | 46 | 206% |
從實驗結果可知,高頻采樣對性能消耗很小,可以不影響用戶體驗。
監控使用Choreographer.FrameCallback, 采樣頻率設52ms),最終結果是性能消耗帶來的影響很小,可忽略:
1)監控代碼本身對主線程有一定的耗時,但影響很小,約0.1ms/S;
2)卡頓監控開啟後,增加0.1%的CPU使用;
3)卡頓監控開啟後,增加Davilk Heap內存約1MB;
4)對於流量,文件可按天寫入,壓縮文件最大約100KB,一天上傳一次
痛點2:海量卡頓堆棧後該如何處理?
卡頓堆棧上報到平臺後,需要對上報的文件進行分析,提取和聚類過程,最終展示到卡頓平臺。前面我們提到,每一次卡頓發生時,會高頻采樣到多個堆棧信息描述著這一個卡頓。做個最小的估算,每天上報收集2000個用戶卡頓文件,每個卡頓文件dump下了用戶遇到的10個卡頓,每個卡頓高頻收集到30個堆棧,這就已經產生20001030=60W個堆棧。按照這個量級發展,一個月可產生上千萬的堆棧信息,每個堆棧還是幾十行的函數調用關系。這麽大量的信息對存儲,分析,頁面展示等均帶來相當大的壓力。很快就能撐爆存儲層,平臺無法展示這麽大量的數據,開發更是沒辦法處理這些多的堆棧問題。因而,海量卡頓堆棧成為我們另外一個面對的難題。
在一個卡頓過程中,一般卡頓發生在某個函數的調用上,在這多個堆棧列表中,我們把每個堆棧都做一次hash處理後進行排重分析,有很大的幾率會是dump到同一個堆棧hash,如下圖:
我們對一個卡頓中多個堆棧進行統計,去重後找出最高重復次數的堆棧,發現堆棧C出現了3次,這次卡頓很有可能就是卡在堆棧3反映的函數調用上。由於采樣頻率不低,因此出現卡頓後一般都有不少的卡頓,如此可找出重復次數最高的堆棧,作為重點分析卡頓問題,從而進行修復。
舉個實際上報數據例子,可以由下圖看到,一個卡頓如序號3,在T1~T2時間段共收集到62個堆棧,我們發現大部分堆棧都是一樣的,於是我們把堆棧hash後嘗試去重,發現排重後只有2個堆棧,而其中某個堆棧重復了59次,我們可以重點關註和處理這個堆棧反映出的卡頓問題。
把一個卡頓抽離成一個關鍵的堆棧的思路,可以大大降低了數據量, 前面提及60W個堆棧就可以縮減為2W個堆棧(2000101=2W)。
按照這個方法,處理後的每個卡頓只剩下一個堆棧,進而每個卡頓都有唯一的標識(hash)。到此,我們還可以對卡頓進行聚類操作,進一步排重和縮小數據量。分類前對每個堆棧,根據業務的不同設置好過濾關鍵字,提取出感興趣的代碼行,去除其他冗余的系統函數後進行歸類。目前主要有兩種方式的分類:
1、按堆棧最外層分類,這種分類方法把同樣入口的函數導致的卡頓收攏到一起,開發修復對應入口的函數來解決卡頓,然而這種方式有一定的風險,可能同樣入口但最終調用不同的函數導致的卡頓則會被忽略;
2、按堆棧最內層分類,這種分類方法能收攏同樣根源問題的卡頓,缺點就是可能忽略調用方可能有多個業務入口,會造成fix不全面。
當然,這兩種方式的聚類,從一定程度上分類大量的卡頓,但不太好控制的是,究竟要取堆棧的多少層作為識別分類。層數越多,則聚類結果變多,分類更細,問題零碎;層數越少,則聚類結果變少,達不到分類的效果。這是一個權衡的過程,實際則按照一定的嘗試效果後去劃分層數,如微信iOS卡頓監控采用的策略是一級分類按最內層倒數2層分類,二級分類按最內層倒數4層。
對於我們產品,目前我們沒有按層數最內或最外來劃分,直接過濾出感興趣的關鍵字的代碼後直接分類。這樣的分類效果下來數據量級在承受範圍內,如之前的2W堆棧可聚類剩下大約2000個(視具體聚類結果)。同時,每天新上報的堆棧都跟歷史數據對比聚合,只過濾出未重復的堆棧,更進一步地縮減上報堆棧的真正存儲量。
卡頓監控系統的處理流程
用戶上報
目前我們的策略是:
1、通過後臺配置下發,灰度0.2%的用戶量進行卡頓監控和上報;
2、如果用戶反饋有卡頓問題,也可實時撈取卡頓日誌來分析;
3、每天灰度的用戶一個機器上報一次,上報後刪除文件不影響存儲空間。
後臺解析
1、主要負責處理上報的卡頓文件,過濾、去重、分類、反解堆棧、入庫等流程;
2、自動回歸修復好的卡頓問題,讀取tapd 卡頓bug單的修復結果,更新平臺展示,計算修復好的卡頓問題,後續版本是否重新出現(修復不徹底)
平臺展示
上報處理後的卡頓展示平臺
<http://test.itil.rdgz.org/welcome/wereadStack/index>
主要展示卡頓處理後的數據:
1、以版本為維度展示卡頓問題列表,按照卡頓上報重復的次數降序列出;
2、歸類後展示每個卡頓的關鍵耗時代碼,也可查看全部堆棧內容;
3、支持操作卡頓記錄,如搜索卡頓,提tapd單,標註已解決等;
4、展示每個版本的卡頓問題修復數據情況,版本分布,監控修復後是否重現等。
自動提單
實際使用中,為了增強跟進效果,我們設立一些規則,比如卡頓重復上報超過100次,卡頓耗時達到1000ms等,自動提tapd bug單給開發處理,系統也會自動更新卡頓問題的修復情況和數據,開發只需定期review tapd bug單處理修復卡頓問題即可,整個卡頓系統從監控,上報,分析,聚類,展示,提單到回歸,整個流程自動化實現,不再需要人工介入。
實際應用效果
1、接入產品:微信讀書,企業微信,QQ郵箱
2、應用場景:現網用戶的監控,發布前測試的監控,每天自動化運行的監控
3、發現問題:三個多月時間,歸類後的卡頓過萬,提bug單約500,開發已解決超過200個卡頓問題
卡頓監控的組件化
考慮到Android卡頓監控的通用性,除了應用於Android WeRead中,我們也推廣到廣研的其他產品中,如企業微信,QQ郵箱。因此,在開發GG的努力下,推出了卡頓監控庫<http://git.code.oa.com/moai/monitor/> ,其他Android產品可快速接入卡頓監控的SDK來監控app卡頓情況。
目前monitor卡頓監控庫主要有監控主線程卡頓情況,獲取平均幀率使用情況,高頻采樣和獲取卡頓信息等基本功能。這裏要註意幾點:
1、采樣堆棧信息的頻率和卡頓耗時的閾值均可在SDK中設置;
2、SDK默認判斷一個卡頓是否發生的耗時閾值是80ms(5*16.6ms)
3、采樣堆棧的頻率是52ms(約3幀+,盡量錯開系統幀率的節奏,堆棧可盡量落到繪制幀過程中)
4、啟動監控後,卡頓日誌就會不斷通過內部的writer輸出,實現MonitorLogWriter.setDelegate才能獲取這些日誌,具體的日誌落地和上報策略因為各個App不同所以沒有集成到SDK中
5、monitor start後一直監控主線程, 包括切換到後臺時也會,直到主動stop或者app被kill。所以在切後臺時要主動stop monitor,切前臺時要重新start
1.組件引入方式
2.主線程卡頓監控的使用方式
1)啟動監控
2)停止監控
3)獲取卡頓信息
app中加入監控卡頓SDK後,會實時輸出卡頓的時間點和堆棧信息,我們將這些信息寫入日誌文件落地,同時每天固定場景上報到服務器,如每天上報一次,用戶打開app後進行上報等策略。收集不同用戶不同手機不同場景下的所有卡頓堆棧信息,可供分析,定位和優化問題。
特別致謝
此文最後特別感謝陽經理(ayangxu)、豪哥(veruszhong)、cginechen對Android卡頓監控組件化的鼎力支持,感謝姑姑(janetjiang)悉心指導與提議!希望卡頓監控系統能越來越多地暴露卡頓問題,在大家的共同努力下不斷提升App的流暢體驗!
相關閱讀
Javascript框架設計思路圖
小程序優化36計
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識
此文已由作者授權騰訊雲+社區發布,更多原文請點擊
搜索關註公眾號「雲加社區」,第一時間獲取技術幹貨,關註後回復1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!
想讓安卓app不再卡頓?看這篇文章就夠了