想讓安卓app不再卡頓?看這篇文章就夠了
歡迎大家前往 ofollow,noindex" target="_blank">騰訊雲+社群 ,獲取更多騰訊海量技術實踐乾貨哦~
本文由 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個堆疊,這就已經產生2000 10 30=60W個堆疊。按照這個量級發展,一個月可產生上千萬的堆疊資訊,每個堆疊還是幾十行的函式呼叫關係。這麼大量的資訊對儲存,分析,頁面展示等均帶來相當大的壓力。很快就能撐爆儲存層,平臺無法展示這麼大量的資料,開發更是沒辦法處理這些多的堆疊問題。因而,海量卡頓堆疊成為我們另外一個面對的難題。
在一個卡頓過程中,一般卡頓發生在某個函式的呼叫上,在這多個堆疊列表中,我們把每個堆疊都做一次hash處理後進行排重分析,有很大的機率會是dump到同一個堆疊hash,如下圖:
我們對一個卡頓中多個堆疊進行統計,去重後找出最高重複次數的堆疊,發現堆疊C出現了3次,這次卡頓很有可能就是卡在堆疊3反映的函式呼叫上。由於取樣頻率不低,因此出現卡頓後一般都有不少的卡頓,如此可找出重複次數最高的堆疊,作為重點分析卡頓問題,從而進行修復。
舉個實際上報資料例子,可以由下圖看到,一個卡頓如序號3,在T1~T2時間段共收集到62個堆疊,我們發現大部分堆疊都是一樣的,於是我們把堆疊hash後嘗試去重,發現排重後只有2個堆疊,而其中某個堆疊重複了59次,我們可以重點關注和處理這個堆疊反映出的卡頓問題。
把一個卡頓抽離成一個關鍵的堆疊的思路,可以大大降低了資料量, 前面提及60W個堆疊就可以縮減為2W個堆疊(2000 10 1=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的流暢體驗!