Android流暢度評估及卡頓優(yōu)化

原文見:博客:Android流暢度評估及卡頓優(yōu)化

1、渲染和流暢概念

Google定義:界面呈現(xiàn)是指從應(yīng)用生成幀并將其顯示在屏幕上的動作。要確保用戶能夠流暢地與應(yīng)用互動,應(yīng)用呈現(xiàn)每幀的時間不應(yīng)超過16ms,以達到每秒60幀的呈現(xiàn)速度(為什么是60fps?)。
如果應(yīng)用存在界面呈現(xiàn)緩慢的問題,系統(tǒng)會不得不跳過一些幀,這會導(dǎo)致用戶感覺應(yīng)用不流暢,我們將這種情況稱為卡頓。

(1)為什么是60fps或16ms?

來源于:Google Android的為什么是60fps?

16ms意味著1000/60hz,相當于60fps。這是因為人眼與大腦之間的協(xié)作無法感知超過60fps的畫面更新。12fps大概類似手動快速翻動書籍的幀率, 這明顯是可以感知到不夠順滑的。24fps使得人眼感知的是連續(xù)線性的運動,這其實是歸功于運動模糊的效果。 24fps是電影膠圈通常使用的幀率,因為這個幀率已經(jīng)足夠支撐大部分電影畫面需要表達的內(nèi)容,同時能夠最大的減少費用支出。 但是低于30fps是 無法順暢表現(xiàn)絢麗的畫面內(nèi)容的,此時就需要用到60fps來達到想要的效果,超過60fps就沒有必要了。如果我們的應(yīng)用沒有在16ms內(nèi)完成屏幕刷新的全部邏輯操作,就會發(fā)生卡頓。

(2)關(guān)于渲染原理

首先要了解Android顯示1幀圖像,所經(jīng)歷的完整過程。


Android渲染機制

如圖所示,屏幕顯示1幀圖像需要經(jīng)歷5個步驟:

  • 定義布局中的組件
  • 將ImageView組件映射成UI對象,加載到內(nèi)存中
  • CPU將UI對象經(jīng)過運算處理成多維矢量圖形
  • GPU柵格化處理
  • 顯示器顯示圖像

常見的丟幀情況: 渲染期間可能出現(xiàn)的情況,渲染大于16ms和小于16ms的情況:

丟幀示例

上圖中應(yīng)該繪制 4 幀數(shù)據(jù) , 但是實際上只繪制了 3 幀 , 實際幀率少了一幀

2、卡頓的標準

判斷APP是否出現(xiàn)卡頓,我們從通用應(yīng)用和游戲兩個緯度的代表公司標準來看,即Google的Android vitals性能指標和地球第一游戲大廠騰訊的PrefDog性能指標。

(1)通用應(yīng)用界面卡頓標準

參考:使用Android Vitals監(jiān)控應(yīng)用的技術(shù)性能

以Google Vitals的卡頓描述為準,即呈現(xiàn)速度緩慢和幀凍結(jié)兩個維度判斷:

  • 呈現(xiàn)速度緩慢:在呈現(xiàn)速度緩慢的幀數(shù)較多的頁面,當超過50%的幀呈現(xiàn)時間超過16ms毫秒時,用戶感官明顯卡頓。
  • 幀凍結(jié):幀凍結(jié)的繪制耗時超過700ms,為嚴重卡頓問題。
  • 卡頓忽略FPS<=2的頁面:因為人的視覺暫留100400ms,即FPS在2.510之間時,所以當FPS低于3時,人眼看到的并不是連續(xù)動作,即使有丟幀現(xiàn)象,也不會察覺。

(2)游戲應(yīng)用界面卡頓標準

來源:騰訊PerfDog使用說明書

PerfDog Jank計算方法:

  • 普通卡頓Jank(同時滿足兩條件):
    • 當前幀耗時>前三幀平均耗時2倍。
    • 當前幀耗時>兩幀電影幀耗時(1000ms/24*2=84ms)。
  • 嚴重卡頓BigJank(同時滿足兩條件):
    • 當前幀耗時>前三幀平均耗時2倍。
    • 當前幀耗時>三幀電影幀耗時(1000ms/24*3=125ms)。

(3)為什么FPS無法判斷是否卡頓?

參考:APP&游戲需要關(guān)注Jank卡頓及卡頓率嗎?

幀率FPS高并不能反映流暢或不卡頓。比如:FPS為50幀,前200ms渲染一幀,后800ms渲染49幀,雖然幀率50,但依然覺得非??D。同時幀率FPS低,并不代表卡頓,比如無卡頓時均勻FPS為15幀。所以平均幀率FPS與卡頓無任何直接關(guān)系)

3、卡頓評估

當了解卡頓的標準以及渲染原理之后,可以得出結(jié)論,只有丟幀情況才能準確判斷是否卡頓。

(1)如何獲取丟幀信息?

參考:Android開發(fā)者 | 測試界面性能

dumpsys 是一種在設(shè)備上運行并轉(zhuǎn)儲需要關(guān)注的系統(tǒng)服務(wù)狀態(tài)信息的 Android 工具。通過向 dumpsys 傳遞 gfxinfo 命令,可以提供 logcat 格式的輸出,其中包含與錄制階段發(fā)生的動畫幀相關(guān)的性能信息。

# 查看幀時間數(shù)據(jù)
adb shell dumpsys gfxinfo < PACKAGE_NAME > framestats
# 幀數(shù)據(jù)重置
adb shell dumpsys gfxinfo < PACKAGE_NAME > reset

聚合幀統(tǒng)計信息

借助 Android 6.0(API 級別 23),該命令可將在整個進程生命周期中收集的幀數(shù)據(jù)的聚合分析輸出到 logcat。例如:

 Stats since: 752958278148ns     
 Total frames rendered: 82189     
 Janky frames: 35335 (42.99%)     
 90th percentile: 34ms     
 95th percentile: 42ms     
 99th percentile: 69ms     
 Number Missed Vsync: 4706     
 Number High input latency: 142     
 Number Slow UI thread: 17270     
 Number Slow bitmap uploads: 1542     
 Number Slow draw: 23342

這些總體統(tǒng)計信息可以得到期間的FPS、Jank比例、各類渲染異常數(shù)量統(tǒng)計。

精確的幀時間信息

命令adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats可提供最近120個幀中,渲染各階段帶有納秒時間戳的幀時間信息。

flags intended_vsync vsync oldest_input_event newest_input_event handle_input_start animation_start perform_traversals_start draw_start sync_queued sync_start issue_draw_commands_start swap_buffers frame_completed
0 27965466202353 27965466202353 27965449758000 27965461202353 27965467153286 27965471442505 27965471925682 27965474025318 27965474588547 27965474860786 27965475078599 27965479796151 27965480589068

關(guān)鍵參數(shù)說明:

  • flags:<font color='red'>FLAGS為0時,總幀時間(ms) = (FRAME_COMPLETED - INTENDED_VSYNC) / 1000000。</font> 如果非零,則該行應(yīng)該被忽略,因為該幀的預(yù)期布局和繪制時間超過16ms,為異常幀。
  • INTENDED_VSYNC:幀的的預(yù)期起點,<font color='red'>監(jiān)測UI線程是否正常</font>。如果與VSYNC值不同,是由于UI線程中的工作使其無法及時響應(yīng)垂直同步信號所造成的。
  • HANDLE_INPUT_START
    • 將輸入事件分派給應(yīng)用的時間戳。
    • 通過觀察此時間戳與 ANIMATION_START 之間的時間差,可以測量應(yīng)用處理輸入事件所花的時間。
    • 如果這個數(shù)字較高(> 2 毫秒),則表明應(yīng)用處理 View.onTouchEvent() <font color='red'>等輸入事件所花的時間太長,這意味著此工作需要進行優(yōu)化或轉(zhuǎn)交給其他線程</font>。請注意,有些情況下(例如,啟動新 Activity 或類似活動的點擊事件),這個數(shù)字較大是預(yù)料之中并且可以接受的。
  • ANIMATION_START
    • 在 Choreographer 中注冊的動畫運行的時間戳。
    • 通過觀察此時間戳與 PERFORM_TRANVERSALS_START 之間的時間差,可以確定評估正在運行的所有動畫(常見動畫有 ObjectAnimator、ViewPropertyAnimator 和 Transitions)所用的時間。
    • <font color='red'>如果這個數(shù)字較高(> 2 毫秒),請檢查您的應(yīng)用是否編寫了任何自定義動畫,或檢查 ObjectAnimator 在對哪些字段設(shè)置動畫并確保它們適用于動畫。</font>
  • PERFORM_TRAVERSALS_START:布局和度量階段完成的時間 = PerformTraversalsStart - DrawStart。<font color='red'>滾動或動畫期間,期望接近0。</font>
  • SYNC_QUEUED
    • 將同步請求發(fā)送給 RenderThread 的時間。
    • 它標記的是將開始同步階段的消息發(fā)送給 RenderThread 的時間點。<font color='red'>如果該時間點與 SYNC_START 的時間差較大(約 > 0.1 毫秒),則意味著 RenderThread 正忙于處理另一幀。它在內(nèi)部用于區(qū)分該幀是因作業(yè)負荷過大而超過了 16 毫秒的預(yù)算時間,還是該幀由于上一幀超過 16 毫秒的預(yù)算時間而停止。</font>
  • SYNC_START
    • 繪制同步階段的開始時間。
    • 如果此時間與 ISSUE_DRAW_COMMANDS_START 之間<font color='red'>相差較大(約 > 0.4 毫秒),通常表示繪制了大量必須上傳到 GPU 的新位圖。</font>
  • ISSUE_DRAW_COMMANDS_START
    • 硬件渲染器開始向 GPU 發(fā)出繪圖命令的時間。
    • 此時間與 FRAME_COMPLETED 之間的時間差讓您可以大致了解應(yīng)用生成的 GPU 工作量。<font color='red'>繪制過度或渲染效果不佳等問題都會在此顯示出來。</font>
  • FrameCompleted:幀的完整時間。幀耗時 = FrameCompleted - IntendedVsync,<font color='red'>要求小于16ms。</font>

(2)如何判斷是否卡頓?

通用應(yīng)用卡頓評估

通過gfxinfo輸出的幀信息,通過定時reset和打印幀信息,可以得到FPS(幀數(shù)/打印間隔時間)、丟幀比例((janky_frames / total_frames_rendered)*100 %)、是否有幀凍結(jié)(幀耗時>700ms)。
根據(jù)第2部分的通用應(yīng)用卡頓標準,可以通過丟幀比例和幀凍結(jié)數(shù)量,準確判斷當前場景是否卡頓。并且通過定時截圖,還可以根據(jù)截圖定位卡頓的具體場景。

流暢度報告截圖

如上圖所示,利用gfxinfo開發(fā)的檢查卡頓的小工具,圖中參數(shù)和卡頓說明如下:

  • FPS = total_frames_renderes:total_frames_renderes為每秒的幀數(shù)量,即FPS。(每秒reset并統(tǒng)計一次)
  • 卡頓為什么去掉FPS<2的數(shù)據(jù):人的視覺暫留100400ms,即FPS在2.510之間時,所以當FPS低于3時,人眼看到的并不是連續(xù)動作,即使有丟幀現(xiàn)象,也不會察覺。
  • UI_score:UI_score = 100 - (janky_frames / total_frames_rendered)*100,根據(jù)Google Vitals呈現(xiàn)速度緩慢的定義,當超過50%的幀呈現(xiàn)時間超過16毫秒,說明呈現(xiàn)速度緩慢。所以,當UI_score<=50時,頁面卡頓。
  • 幀凍結(jié):通過每秒的max_frame_time判斷,當幀凍結(jié)的繪制耗時超過700ms,為嚴重卡頓問題。

游戲應(yīng)用卡頓評估

根據(jù)上面對gfxinfo的幀信息解析,可以準確計算出每一幀的耗時。從而可以開發(fā)出滿足騰訊PerfDog中關(guān)于普通卡頓和嚴重卡頓的判斷。

游戲卡頓定義

依賴定時截圖,即可準確定位卡頓場景。如下圖所示(此處以PerfDog截圖示例):

PerfDog截圖

4、卡頓定位工具和高效定位方法

通過第3部分的卡頓評估方法,我們可以定位到卡頓場景,但是如何定位到具體卡頓原因呢。

首先了解卡頓問題定位工具,然后再了解常見的卡頓原因,即可通過復(fù)現(xiàn)卡頓場景的同時,用工具去定位具體卡頓問題。

(1)卡頓問題定位工具

gfxinfo幀信息
  • Systrace或Perfetto :記錄短時間內(nèi)的設(shè)備活動,匯總了 Android 內(nèi)核中的數(shù)據(jù),例如 CPU 調(diào)度程序、磁盤活動和應(yīng)用線程。
    • Perfetto是Android 10 中引入的全新平臺級跟蹤工具
systrace報告
  • LayoutInspect :檢測動態(tài)布局層次結(jié)構(gòu)、調(diào)查資源屬性值在源代碼中的來源位置、在運行時對應(yīng)用的視圖層次結(jié)構(gòu)進行高級 3D 可視化。
LayoutInspect報告
  • BlockCanary :檢測主線程上的各種卡頓問題
BlockCanary輸出的卡頓信息
  • CPU性能剖析器 :監(jiān)控應(yīng)用進程中的每個線程,執(zhí)行的方法 (Java) 或函數(shù) (C/C++),以及每個方法或函數(shù)在其執(zhí)行期間消耗的 CPU 資源。還可以使用方法和函數(shù)跟蹤數(shù)據(jù)來識別調(diào)用方和被調(diào)用方??梢允褂眠@些信息來確定哪些方法或函數(shù)過于頻繁地調(diào)用通常會消耗大量資源的特定任務(wù),并優(yōu)化應(yīng)用的代碼以避免不必要的工作。
CPU記錄的線程信息
  • GPU 渲染模式分析工具 :以滾動直方圖的形式直觀地顯示渲染界面窗口幀所花費的時間(以每幀 16 毫秒的速度作為對比基準),可定位動畫渲染階段的具體問題(比如:輸入處理耗時問題、界面線程問題、視圖繪制問題等)。
GPU渲染報告

(2)如何高效定位卡頓問題

重點就是,充分利用gfxinfo輸出的幀信息,對卡頓問題進行分類。

  • INTENDED_VSYNC
    • 線程問題:如果此值不同于 VSYNC,則表示界面線程中發(fā)生的工作使其無法及時響應(yīng) Vsync 信號。
    • 推薦定位工具:CPU性能剖析器查看線程中耗時較多的方法或函數(shù)。
  • HANDLE_INPUT_START
    • 輸出時間處理時間長:該值與ANIMATION_START差值>2ms,則表明應(yīng)用處理 View.onTouchEvent() 等輸入事件所花的時間太長,這意味著此工作需要進行優(yōu)化或轉(zhuǎn)交給其他線程。
    • 注意事項:有些情況下(例如,啟動新 Activity 或類似活動的點擊事件),這個數(shù)字較大是預(yù)料之中并且可以接受的。
    • 推薦定位工具:CPU性能剖析器查看線程中View.onTouchEvent(),并優(yōu)化代碼或轉(zhuǎn)交給其他線程處理。
  • ANIMATION_START
    • 動畫問題:該值與PERFORM_TRANVERSALS_START差值>2ms,自定義動畫問題 或 不適合的字段設(shè)置動畫問題。
    • 推薦定位工具:GPU 渲染模式分析工具,可定位輸入處理耗時問題、界面線程問題、視圖繪制問題等具體的問題范疇。
  • PERFORM_TRAVERSALS_START
    • 布局問題:該值與DRAW_START如果>0,表明完成布局和測量階段耗時較多。
    • 推薦定位工具:使用GPU渲染分析工具確認是否Draw或Measure/Layout耗時較多,Draw較多說明更新的視圖太多或View的OnDraw方法做了耗時操作; Measure/Layout耗時較多,說明布局可能有嚴重性能問題,使用LayoutInspect檢查布局是否過于復(fù)雜,減少嵌套層次和控件個數(shù)。
  • SYNC_QUEUED
    • 幀作業(yè)負荷較大問題:該值與 SYNC_START 的時間差較大(約 > 0.1 毫秒),則意味著 RenderThread 正忙于處理另一幀。它在內(nèi)部用于區(qū)分該幀是因作業(yè)負荷過大而超過了 16 毫秒的預(yù)算時間,還是該幀由于上一幀超過 16 毫秒的預(yù)算時間而停止。
    • 推薦定位工具:如果是因為當前幀作業(yè)負荷較大導(dǎo)致耗時較多,觀察其他參數(shù)具體定位問題。
  • SYNC_START
    • 需要上傳到GPU的新位圖較多:如果此時間與 ISSUE_DRAW_COMMANDS_START 之間相差較大(約 > 0.4 毫秒),通常表示繪制了大量必須上傳到 GPU 的新位圖。
    • 推薦定位工具:GPU渲染分析工具,具體定位渲染階段問題。

(3)主要卡頓原因

主要參考:Android卡頓檢測及優(yōu)化

了解了高效定位卡頓的方法和卡頓問題定位工具,再熟悉一下常見的卡頓原因,可以更熟練的定位和優(yōu)化卡頓。

A. 系統(tǒng)層面卡頓原因

SurfaceFlinger 主線程耗時

SurfaceFlinger 負責 Surface 的合成,一旦 SurfaceFlinger 主線程調(diào)用超時,就會產(chǎn)生掉幀。
SurfaceFlinger 主線程耗時會也會導(dǎo)致 hwc service 和 crtc 不能及時完成,也會阻塞應(yīng)用的 binder 調(diào)用,如 dequeueBuffer、queueBuffer 等。

后臺活動進程太多導(dǎo)致系統(tǒng)繁忙

后臺進程活動太多,會導(dǎo)致系統(tǒng)非常繁忙,cpu \ io \ memory 等資源都會被占用,這時候很容易出現(xiàn)卡頓問題,這也是系統(tǒng)這邊經(jīng)常會碰到的問題。
dumpsys cpuinfo 可以查看一段時間內(nèi) cpu 的使用情況:

image.png

主線程調(diào)度不到 , 處于 Runnable 狀態(tài)

當線程為 Runnable 狀態(tài)的時候,調(diào)度器如果遲遲不能對齊進行調(diào)度,那么就會產(chǎn)生長時間的 Runnable 線程狀態(tài),導(dǎo)致錯過 Vsync 而產(chǎn)生流暢性問題。

System 鎖

system_server 的 AMS 鎖和 WMS 鎖 , 在系統(tǒng)異常的情況下 , 會變得非常嚴重 , 如下圖所示 , 許多系統(tǒng)的關(guān)鍵任務(wù)都被阻塞 , 等待鎖的釋放 , 這時候如果有 App 發(fā)來的 Binder 請求帶鎖 , 那么也會進入等待狀態(tài) , 這時候 App 就會產(chǎn)生性能問題 ; 如果此時做 Window 動畫 , 那么 system_server 的這些鎖也會導(dǎo)致窗口動畫卡頓。


image.png

Layer過多導(dǎo)致 SurfaceFlinger Layer Compute 耗時

Android P 修改了 Layer 的計算方法 , 把這部分放到了 SurfaceFlinger 主線程去執(zhí)行, 如果后臺 Layer 過多,就會導(dǎo)致 SurfaceFlinger 在執(zhí)行 rebuildLayerStacks 的時候耗時 , 導(dǎo)致 SurfaceFlinger 主線程執(zhí)行時間過長。


image.png

B. 應(yīng)用層面卡頓原因

主線程執(zhí)行時間長

主線程執(zhí)行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超時都會導(dǎo)致卡頓 。

  • Measure \ Layout 耗時\超時
  • draw耗時
  • Animation回調(diào)耗時
  • View 初始化耗時
  • List Item 初始化耗時
  • 主線程操作數(shù)據(jù)庫

主線程 Binder 耗時

Activity resume 的時候, 與 AMS 通信要持有 AMS 鎖, 這時候如果碰到后臺比較繁忙的時候, 等鎖操作就會比較耗時, 導(dǎo)致部分場景因為這個卡頓, 比如多任務(wù)手勢操作。


image.png

WebView 性能不足

應(yīng)用里面涉及到 WebView 的時候, 如果頁面比較復(fù)雜, WebView 的性能就會比較差, 從而造成卡頓。


image.png

幀率與刷新率不匹配

如果屏幕幀率和系統(tǒng)的 fps 不相符 , 那么有可能會導(dǎo)致畫面不是那么順暢. 比如使用 90 Hz 的屏幕搭配 60 fps 的動畫。

image.png

5、卡頓優(yōu)化建議

由上面的分析可知對象分配、垃圾回收(GC)、線程調(diào)度以及Binder調(diào)用 是Android系統(tǒng)中常見的卡頓原因,因此卡頓優(yōu)化主要以下幾種方法,更多的要結(jié)合具體的應(yīng)用來進行:

(1)布局優(yōu)化

  • 通過減少冗余或者嵌套布局來降低視圖層次結(jié)構(gòu)。比如使用約束布局代替線性布局和相對布局。
  • 用 ViewStub 替代在啟動過程中不需要顯示的 UI 控件。
  • 使用自定義 View 替代復(fù)雜的 View 疊加。
  • 減少嵌套層次和控件個數(shù)。
  • 使用Tags:Merge標簽減少布局嵌套層次,ViewStub標簽推遲創(chuàng)建對象、延遲初始化、節(jié)省內(nèi)存等。
  • 減少過度繪制

(2)減少主線程耗時操作

  • 主線程中不要直接操作數(shù)據(jù)庫,數(shù)據(jù)庫的操作應(yīng)該放在數(shù)據(jù)庫線程中完成。
  • sharepreference盡量使用apply,少使用commit,可以使用MMKV框架來代替sharepreference。
  • 網(wǎng)絡(luò)請求回來的數(shù)據(jù)解析盡量放在子線程中,不要在主線程中進行復(fù)制的數(shù)據(jù)解析操作。
  • 不要在activity的onResume和onCreate中進行耗時操作,比如大量的計算等。

(3)列表優(yōu)化

  • RecyclerView使用優(yōu)化,使用DiffUtil和notifyItemDataSetChanged進行局部更新等。

(4)內(nèi)存優(yōu)化

  • 減少小對象的頻繁分配和回收操作。
  • 被頻繁調(diào)用的緊密的循環(huán)里,避免對象分配來降低GC的壓力。

6、名詞解釋

(1)幀

在計算機和通信領(lǐng)域,幀是一個包括“幀同步串行”的數(shù)字數(shù)據(jù)傳輸單元或數(shù)字數(shù)據(jù)包。
在視頻領(lǐng)域,電影、電視、數(shù)字視頻等可視為隨時間連續(xù)變換的許多張畫面,其中幀是指每一張畫面。

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