安卓幀渲染數據獲取方式小結

首先解釋一下文章標題中的“幀渲染數據”。
“幀渲染數據”是指,完成渲染一幀的耗時。這是計算幀率的基礎數據。

截止到 8.0 系統,安卓原生提供 API 或者自帶的工具,甚至是統計性能的後臺 Google Vitals,都沒有提供直接獲取幀率的功能。但是這些 API 或工具,直接或者間接的提供了獲取每一幀渲染耗時的功能,開發者需要做二次計算才能得到幀率。至於爲什麼並給出幀率這個數據,我會在後文中給出自己的推測。

如果我們拿到了每一幀的耗時,我們就拿到了兩個數據:某段連續時間 deltT 內渲染完成的幀數 n,那麼 n / deltT 就是幀率。deltT 的選取上具有很大靈活性,deltT 應該設置爲 1 秒,還是 2 秒?亦或是,n 固定爲 1,相應的 deltT 設置該幀的耗時?不同的選取方法,得到的幀率值也不盡相同。比如第 n 幀耗時 tn,對 1/tn > 60 ? 60 : 1/tn 累加求和然後求均值,實際操作後會發現這種方案受某超時幀影響嚴重,如果某幀耗時較大,會大大拉低最後的 fps 值。

注意,雖然安卓原生系統沒有直接提供幀率這個性能指標數據,但是某些第三方 Rom,比如魅族 M2 Note 手機上,Flyme 系統提供了幀率數據。

下面講下獲取幀數據的策略和對應的實現方式。

兩種策略四種方式

目前,獲取幀數據的策略由 Choreographer.FrameCallback 和 GraphicsBinder 兩種。

Choreographer.FrameCallback 的代表作是開源庫 TinyDancer 和美團外賣的 Hertz(卡頓偵測)。

GraphicsBinder 的代表方式是 Profile GPU 和 FrameMetrics。

下面分別進行介紹。

Choreographer$FrameCallback

這種方式起源於 Facebook 在 DroidCon 的分享:《Road to 60fps》。在這之後,基於這個思路獲取幀數據的各種開源庫便如雨後春筍般出現了。

從 16ms 說起

多數設備的屏幕刷新頻率是 60Hz,即每秒刷新 60 次,每隔 16.67 ms 刷新一次。如果下一幀能夠在 16.67 ms 內渲染完成,每次刷新都能展示新的幀,在用戶看來 app 流暢運行,否則第 N+1 次屏幕刷新將繼續展示第 N幀(第 N+1 幀尚未渲染完成),將出現掉幀、卡頓現象。

但是需要注意的是,並不是所有的設備的刷新頻率都是 60hz,相應的 60fps 對某些機型是不適用的,即某些機型上你永遠無法達到 60fps(Galaxy core 2 33/60,Nexus 5 55/60,Nexus 4 49/60)。

這個思路牽涉兩個核心類/接口:

  • Choreographer
  • Choreographer$FrameCallback

一次屏幕刷新完成後,將產生 VSync 信號並通知 Choreographer。
Choreographer 收到通知依次處理 Input、Animation、Draw,這三個過程都是通過 FrameCallback 回調的方式完成的。在 Draw 過程中,具體是執行 ViewRootImpl#performTraversals() 方法,完成視圖樹的 measure、layout、draw 流程。
而 FrameCallback#doFrame(long frameTimeNanos) 方法中可以得到 VSync 到來的時間戳,這樣就能得到連續兩幀開始渲染之間的間隔,將該值近似作爲上一幀的渲染耗時。
實現 FrameCallback 接口,並通過 Choreographer#postFrameCallback() 方法將其跟 Input、Animation、Draw 這些回調一起塞入主線程的消息隊列,就能源源不斷的獲取每一幀的渲染時間戳,每一個 VSync 的時間戳代表一幀,這樣可以得到某段時間內渲染完成的幀數,二者相除即可得到幀率。


(上圖摘自《Road to 60fps》

GraphicsBinder

Profile GPU

通過 Profile GPU 可以獲得每幀渲染耗時的詳細數據,即渲染的每個階段的耗時情況,方便開發者定位性能瓶頸。
幀渲染耗時柱狀圖
有兩種方式可以查看柱狀圖:

  1. 在手機上查看,手機設置—開發者選項— GPU 呈現模式分析(或 GPU 顯示配置文件)— 勾選“顯示條形圖”;
  2. 在 Android Studio 中查看,打開 GPU 呈現模式分析 — 勾選“在 adb shell dumpsys gfxinfo 中”,柱狀圖會顯示在控制檯的 GPU Monitor 區域;

5.0 及以下系統

4.3 系統上效果(在 GPU Monitor 中的效果,綠線表示 16ms,紅線表示 33ms):
這裏寫圖片描述

5.0 上效果(在 GPU Monitor 中的效果):
這裏寫圖片描述

各個色塊所代表的含義及該色塊過大的可能原因:

色塊 階段 含義
Process 表示 CPU 在等待 GPU 完成渲染的耗時;該階段耗時大表示 app 在 GPU 中做了過多的操作。
這裏寫圖片描述 Execute Android 2d 渲染引擎利用 OpenGL 繪製和刷新 DisplayList 的耗時。該階段耗時大表示 DisplayList 過多、執行時間過長。
這裏寫圖片描述 XFer 上傳 bitmap 到 GPU 的耗時。耗時過多表示 app 在加載過多的圖形圖片。
這裏寫圖片描述 Update 創建和更新視圖 DisplayList 的耗時。耗時過多可能是由於自定義 view 繪製過多,或者 onDraw() 方法裏面操作過多。

6.0 及以上系統
在 GPU Monitor 中的效果:
這裏寫圖片描述

各個色塊所代表的含義及該色塊過大的可能原因:

色塊 階段 含義
這裏寫圖片描述 Swap Buffers 表示 CPU 在等待 GPU 完成渲染的耗時;該階段耗時大表示 app 在 GPU 中做了過多的操作。
這裏寫圖片描述 Command Issue Android 2d 渲染引擎利用 OpenGL 繪製和刷新 DisplayList 的耗時。該階段耗時大表示 DisplayList 過多、執行時間過長。
這裏寫圖片描述 Sync & Upload 上傳 bitmap 到 GPU 的耗時。耗時過多表示 app 在加載過多的圖形圖片。
這裏寫圖片描述 Draw 創建和更新視圖 DisplayList 的耗時。耗時過多可能是由於自定義 view 繪製過多,或者 onDraw() 方法裏面操作過多。
這裏寫圖片描述 Measure / Layout 視圖樹執行 onMeasure() 和 onLayout() 方法的耗時;耗時過多表示視圖樹在這兩個階段效率較低。
這裏寫圖片描述 Animation 執行動畫的耗時。耗時過多可能是因爲自定義動畫運行效率較低,或者屬性刷新出現異常狀況。
這裏寫圖片描述 Input Handling 執行輸入時間回調的耗時。耗時過多可能是因爲 app 在處理過多的用戶輸入時間,可以考慮將這些事件放到其他線程中進行處理。
這裏寫圖片描述 Misc Time / VSync Delay 執行連續兩幀之間的操作耗時。耗時過多可能是因爲 UI 線程操作過多,可以考慮將這些操作放到其他線程中進行處理。

在 5.0 上執行 gfxinfo 命令,得到的即爲渲染一幀所經過的各個階段的耗時情況(單位毫秒):

adb shell dumpsys gfxinfo com.demo.app

Draw Prepare Process Execute
0.51 0.69 4.52 0.40
0.43 1.20 3.90 0.36
0.42 0.64 3.70 0.37
0.41 0.68 4.08 0.57
0.46 1.24 3.79 0.35

在 7.0 上執行:

adb shell dumpsys gfxinfo com.demo.app

Stats since: 115689258308387ns
Total frames rendered: 138
Janky frames: 114 (82.61%)
50th percentile: 19ms
90th percentile: 150ms
95th percentile: 200ms
99th percentile: 300ms
Number Missed Vsync: 40
Number High input latency: 2
Number Slow UI thread: 40
Number Slow bitmap uploads: 2
Number Slow issue draw commands: 70


Draw Prepare Process Execute
50.00 0.40 5.48 3.78
50.00 0.77 1.66 3.97
50.00 4.31 2.01 2.59
50.00 5.29 9.59 4.39
50.00 2.95 3.07 8.06
50.00 1.76 1.93 3.12

在 7.0 系統上帶上 framestats 參數可以獲取最近的 120 幀數據:

adb shell dumpsys gfxinfo com.demo.app framestats

Stats since: 101631537739178ns
Total frames rendered: 42
Janky frames: 31 (73.81%)
50th percentile: 17ms
90th percentile: 19ms
95th percentile: 21ms
99th percentile: 34ms
Number Missed Vsync: 2
Number High input latency: 2
Number Slow UI thread: 3
Number Slow bitmap uploads: 0
Number Slow issue draw commands: 27

---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,
0,101647039145922,101647039145922,101647018084000,101647034145922,101647039815217,101647041206884,101647041424071,101647041635530,101647042167821,101647044205842,101647045030842,101647051882405,101647055263134,
0,101647054735049,101647054735049,101647039692000,101647049735049,101647055237613,101647056265738,101647056492821,101647056665738,101647057101676,101647057342821,101647058124071,101647066840738,101647074210530,
0,101647071403801,101647071403801,101647050345000,101647066403801,101647071899592,101647074218342,101647074530321,101647074697509,101647075244905,101647075473030,101647076719384,101647082193342,101647090448030,
0,101647089118048,101647089118048,101647072068000,101647084118048,101647089415738,101647090049071,101647090219905,101647090331884,101647090610009,101647090709488,101647091358446,101647095696988,101647107083967,
0,101647105786017,101647105786017,101647093579000,101647100786017,101647106096988,101647106731363,101647106896988,101647107007405,101647107266780,101647132519905,101647133169905,101647137525113,101647140328759,
---PROFILEDATA---

第一部分是卡頓的統計數據,包括掉幀率、不同分位值對應的耗時;第二部分(PROFILEDATA)是詳細數據,即繪製一幀所經過的各個階段的起始時間戳,最後一項減去第二項即爲該幀的耗時(單位納秒);

除了 framestats 參數,執行 reset 參數可以清楚幀數據緩存,重新開始記錄幀數據。

8.0 上執行得到的數據,跟 7.0 相比在 PROFILEDATA 部分會多處兩列:

0,774613991199371,774613991199371,9223372036854775807,0,774613992126069,774613992376069,774613992786486,774613994317736,774613995501069,774613995666173,774613998539611,774614003405757,774614008956798,731000,2023000,
0,774614007852016,774614007852016,9223372036854775807,0,774614008804194,774614009077111,774614009444298,774614010829715,774614011721903,774614011951069,774614015110444,774614019814090,774614023723986,669000,1154000,
0,774614024506735,774614024506735,9223372036854775807,0,774614025356798,774614025628153,774614026001069,774614027616173,774614028856798,774614029033361,774614031576069,774614040898465,774614046081278,2293000,955000,
0,774614207699405,774614207699405,9223372036854775807,0,774614208585444,774614208868778,774614209313048,774614210661486,774614211692736,774614212157319,774614214649507,774614221605757,774614226720861,687000,888000,
0,774614290967221,774614290967221,9223372036854775807,0,774614291604715,774614291821902,774614292169298,774614293051590,774614293895340,774614294029715,774614296510444,774614301460965,774614305854194,468000,1607000,
0,774614407547237,774614407547237,9223372036854775807,0,774614408341694,774614408531277,774614408836486,774614409757840,774614410454715,774614410648986,774614413372423,774614417038048,774614420630236,489000,848000,
0,774614424202018,774614424202018,9223372036854775807,0,774614424797423,774614424998465,774614425294819,774614426081277,774614426981798,774614427118257,774614428749507,774614433750027,774614437724507,415000,932000,
0,774614590744196,774614590744196,9223372036854775807,0,774614591398465,774614591572423,774614591851590,774614592689090,774614593398465,774614593537527,774614595425548,774614600326069,774614604530757,685000,947000,
0,774614607398691,774614607398691,9223372036854775807,0,774614608457840,774614608659402,774614608928152,774614609817215,774614610520861,774614610642736,774614613067215,774614616383361,774614619838569,456000,998000,
0,774614807246899,774614807246899,9223372036854775807,0,774614808425548,774614808673986,774614809040132,774614809862527,774614810560444,774614810714090,774614812768777,774614816448986,774614819731798,562000,821000,
0,774614823900602,774614823900602,9223372036854775807,0,774614824794819,774614824975548,774614825235444,774614825978152,774614826975548,774614827372423,774614829708882,774614834403673,774614837898465,464000,1146000,
0,774614890517842,774614890517842,9223372036854775807,0,774614892127632,774614892273465,774614892565132,774614893267215,774614893978673,774614894082319,774614896123465,774614900918257,774614905893257,499000,1286000,

FrameMetrics

從 7.0(API 24)開始,安卓 SDK 新增 OnFrameMetricsAvailableListener 接口用於提供幀繪製各階段的耗時,數據源與 GPU Profile 相同。
回調接口爲 Window.FrameMetrics:

public interface OnFrameMetricsAvailableListener {
    void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation);
}

FrameMetics 存儲瞭如下數據:

階段 含義(納秒) 備註
ANIMATION_DURATION 動畫耗時
COMMAND_ISSUE_DURATION 執行 OpenGL 命令和 DisplayList 耗時
DRAW_DURATION 創建和更新 DisplayList 耗時
FIRST_DRAW_FRAME 布爾值,標誌該幀是否爲此 Window 繪製的第一幀 一般忽略此幀
INPUT_HANDLING_DURATION 處理用戶輸入操作的耗時
INTENDED_VSYNC_TIMESTAMP 預期 VSync 到來的時間戳 API >=26 可用
LAYOUT_MEASURE_DURATION layout/measure 耗時
SWAP_BUFFERS_DURATION CPU 在等待 GPU 完成渲染的耗時
SYNC_DURATION 上傳 bitmap 到 GPU 的耗時
TOTAL_DURATION 整幀渲染耗時
UNKNOWN_DELAY_DURATION 未知延遲
VSYNC_TIMESTAMP VSync 實際到來的時間戳 API >=26 可用

比如使用 ActivityFrameMetrics 的效果:

這裏寫圖片描述

該方法可以在 Application 統一完成初始化,無需各個頁面單獨設置。

優勢:
1. 官方推薦方式;
2. 能夠線上使用;
3. 不限於 120 幀;
4. 能獲取第三方應用如競品應用的幀渲染數據;

劣勢:
1. 7.0 及以上系統;
2. 要開啓硬件加速;

需要注意的是,雖然 FrameMetrics 與 gfxinfo 的數據同源,但是二者在計算幀率時也有差別。FrameMetrics 獲得的是單個幀的數據,正常情況下只能拿到渲染一幀的各個階段的數據(加起來是該幀總耗時),而不能像 gfxinfo 那樣拿到每一幀渲染的起始時間戳。如果用橫線表示每幀的渲染,則對開發者而言,FrameMetrics 獲得的幀繪製過程是:
—— ———— —— ————

而其實際渲染過程,也即是 gfxinfo 獲取的渲染過程可能是:
——        ——
     ————    ————

分兩種情況(具體見 Android5.0中 hwui 中 RenderThread 工作流程 文尾表述):

  • 主線程在完成第一幀自己負責的部分後交給 Render Thread 線程,然後自己馬不停蹄的繼續繪製第二幀;
  • 在完成第一幀自己負責的部分後交給 Render Thread 線程,然後等待 Render Thread 完成第一幀剩餘的工作後在進行第二幀的繪製;

一次滑動過程中,上述兩種情況可能均存在。

故在使用 n/t (以上圖爲例 n = 4) 計算幀率時,前者的 t 是 n 幀耗時之和(n 幀的渲染是一個串行過程),後者的 t 是最後一幀渲染結束的時間戳減去第一幀渲染開始時的時間戳,前者算出的 t 大於後者算出的 t,得出的幀率也就不一樣。原因在於前者未提供獲取每一幀渲染起始的時間戳,我們只能以將其作爲一個有先後順序的串行的渲染過程。

另一個需要注意的地方是,log 中的幀並不是連續的,每幀繪製中間會有間隔,較大的間隔。比如我們以 8.0 系統(倒數第三列數據是渲染結束時的時間戳)上獲取某次連續滑動得到的幀數據爲例:

0,774613991199371,774613991199371,9223372036854775807,0,774613992126069,774613992376069,774613992786486,774613994317736,774613995501069,774613995666173,774613998539611,774614003405757,774614008956798,731000,2023000,
0,774614007852016,774614007852016,9223372036854775807,0,774614008804194,774614009077111,774614009444298,774614010829715,774614011721903,774614011951069,774614015110444,774614019814090,774614023723986,669000,1154000,
0,774614024506735,774614024506735,9223372036854775807,0,774614025356798,774614025628153,774614026001069,774614027616173,774614028856798,774614029033361,774614031576069,774614040898465,774614046081278,2293000,955000,
0,774614207699405,774614207699405,9223372036854775807,0,774614208585444,774614208868778,774614209313048,774614210661486,774614211692736,774614212157319,774614214649507,774614221605757,774614226720861,687000,888000,
0,774614290967221,774614290967221,9223372036854775807,0,774614291604715,774614291821902,774614292169298,774614293051590,774614293895340,774614294029715,774614296510444,774614301460965,774614305854194,468000,1607000,
0,774614407547237,774614407547237,9223372036854775807,0,774614408341694,774614408531277,774614408836486,774614409757840,774614410454715,774614410648986,774614413372423,774614417038048,774614420630236,489000,848000,
0,774614424202018,774614424202018,9223372036854775807,0,774614424797423,774614424998465,774614425294819,774614426081277,774614426981798,774614427118257,774614428749507,774614433750027,774614437724507,415000,932000,
0,774614590744196,774614590744196,9223372036854775807,0,774614591398465,774614591572423,774614591851590,774614592689090,774614593398465,774614593537527,774614595425548,774614600326069,774614604530757,685000,947000,
0,774614607398691,774614607398691,9223372036854775807,0,774614608457840,774614608659402,774614608928152,774614609817215,774614610520861,774614610642736,774614613067215,774614616383361,774614619838569,456000,998000,
0,774614807246899,774614807246899,9223372036854775807,0,774614808425548,774614808673986,774614809040132,774614809862527,774614810560444,774614810714090,774614812768777,774614816448986,774614819731798,562000,821000,
0,774614823900602,774614823900602,9223372036854775807,0,774614824794819,774614824975548,774614825235444,774614825978152,774614826975548,774614827372423,774614829708882,774614834403673,774614837898465,464000,1146000,
0,774614890517842,774614890517842,9223372036854775807,0,774614892127632,774614892273465,774614892565132,774614893267215,774614893978673,774614894082319,774614896123465,774614900918257,774614905893257,499000,1286000,

取每幀的開始和結束時間戳換算成毫秒並將第一幀的開始時間戳作爲0,用 highcharts 繪製出甘特圖,html 代碼和效果圖分別如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <!--<script src="https://code.highcharts.com/highcharts.js"></script>-->
    <!--<script src="https://code.highcharts.com/highcharts-more.js"></script>-->
    <!--<script src="https://code.highcharts.com/modules/exporting.js"></script>-->

    <!--<script type="text/javascript" src="../../../template/highcharts.js"></script>-->
    <!--<script type="text/javascript" src="../../../template/highcharts-more.js"></script>-->
    <!--<script type="text/javascript" src="../../../template/exporting.js"></script>-->

    <script type="text/javascript" src="jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="highcharts.js"></script>
    <script type="text/javascript" src="highcharts-more.js"></script>
    <script type="text/javascript" src="exporting.js"></script>

</head>
<body style="margin:0;padding:20px 0 0 0">

<br/>
<center>
    <div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>

    <div id="frames_log">
        0,774613991199371,774613991199371,9223372036854775807,0,774613992126069,774613992376069,774613992786486,774613994317736,774613995501069,774613995666173,774613998539611,774614003405757,774614008956798,731000,2023000,<br>
        0,774614007852016,774614007852016,9223372036854775807,0,774614008804194,774614009077111,774614009444298,774614010829715,774614011721903,774614011951069,774614015110444,774614019814090,774614023723986,669000,1154000,<br>
        0,774614024506735,774614024506735,9223372036854775807,0,774614025356798,774614025628153,774614026001069,774614027616173,774614028856798,774614029033361,774614031576069,774614040898465,774614046081278,2293000,955000,<br>
        0,774614207699405,774614207699405,9223372036854775807,0,774614208585444,774614208868778,774614209313048,774614210661486,774614211692736,774614212157319,774614214649507,774614221605757,774614226720861,687000,888000,<br>
        0,774614290967221,774614290967221,9223372036854775807,0,774614291604715,774614291821902,774614292169298,774614293051590,774614293895340,774614294029715,774614296510444,774614301460965,774614305854194,468000,1607000,<br>
        0,774614407547237,774614407547237,9223372036854775807,0,774614408341694,774614408531277,774614408836486,774614409757840,774614410454715,774614410648986,774614413372423,774614417038048,774614420630236,489000,848000,<br>
        0,774614424202018,774614424202018,9223372036854775807,0,774614424797423,774614424998465,774614425294819,774614426081277,774614426981798,774614427118257,774614428749507,774614433750027,774614437724507,415000,932000,<br>
        0,774614590744196,774614590744196,9223372036854775807,0,774614591398465,774614591572423,774614591851590,774614592689090,774614593398465,774614593537527,774614595425548,774614600326069,774614604530757,685000,947000,<br>
        0,774614607398691,774614607398691,9223372036854775807,0,774614608457840,774614608659402,774614608928152,774614609817215,774614610520861,774614610642736,774614613067215,774614616383361,774614619838569,456000,998000,<br>
        0,774614807246899,774614807246899,9223372036854775807,0,774614808425548,774614808673986,774614809040132,774614809862527,774614810560444,774614810714090,774614812768777,774614816448986,774614819731798,562000,821000,<br>
        0,774614823900602,774614823900602,9223372036854775807,0,774614824794819,774614824975548,774614825235444,774614825978152,774614826975548,774614827372423,774614829708882,774614834403673,774614837898465,464000,1146000,<br>
        0,774614890517842,774614890517842,9223372036854775807,0,774614892127632,774614892273465,774614892565132,774614893267215,774614893978673,774614894082319,774614896123465,774614900918257,774614905893257,499000,1286000,<br>
    </div>
</center>


<script>


    var lines = $("#frames_log").html().trim().split('\n')

    var startIndex = 1
    var endIndex = 13

    var baseStamp

    var logs = []

    for (var i = 0; i < lines.length; i++) {
        var line = lines[i]
        var stamps = line.split(',')

        var pair = []
        pair[0] = stamps[startIndex]
        pair[1] = stamps[endIndex]

        if (i == 0) {
            baseStamp = pair[0]
        }

        logs[i] = pair
    }

    for (var i = 0; i < logs.length; i++) {
        pair = logs[i]

        for (var j = 0; j < pair.length; j++) {
            pair[j] = (pair[j] - baseStamp) / 1000000
        }

        console.log(pair)
    }


Highcharts.chart('container', {

    chart: {
        type: 'columnrange',
        inverted: true
    },

    title: {
        text: 'frame rendering gant'
    },

    subtitle: {
        text: 'from 8.0'
    },

    xAxis: {
        categories: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
    },

    scrollbar: {
            enabled: true
    },

    yAxis: {
        title: {
            text: 'ms'
        }
    },

    tooltip: {
        valueSuffix: ''
    },

    plotOptions: {
        columnrange: {
            dataLabels: {
                enabled: true,
                formatter: function () {
                    return "";
                }
            }
        }
    },

    legend: {
        enabled: false
    },

    series: [{
        name: 'cost',
        data: logs
    }]

});
</script>

</body>
</html>

這裏寫圖片描述

可以看出,其中有多處超過 100ms 的間隙。

更進一步,我們知道只要耗時大於 16.6 ms,就會發生掉幀,按正常邏輯來說,掉幀越多,幀率會越低。考慮下圖這種極端情況:

這裏寫圖片描述

渲染了 A B C D 共 4 幀,每幀都耗時 2 個 VSync 週期,即每幀都出現了掉幀,掉幀率 100%,但是由於 UI 線程和 Render 線程並行進行,5 個 VSync 週期渲染完成了 4 幀。如果不只是 4 幀,而是 n 幀,n 較大,那麼可以認爲 n 個 VSync 週期渲染完成了 n 幀。幀率就是 60,但是掉幀率是 100%。這顯然與“掉幀越多,幀率會越低”是矛盾的,甚至是違反常識的。所以,我認爲,在 Render 線程存在的情況(即開啓硬件加速的情況下),幀率已經無法作爲表徵 UI 性能的指標了。這也可以從側面解釋,爲什麼 gfxInfo 也好,Google Vitals 也好,都給出了幀渲染數據、掉幀率等,但是就是沒給出幀率這個至關重要的數據指標。當然這只是一己之見,歡迎討論指正。

通過 onDraw() 獲取幀數據

可能有些同學會心生疑問:就這這兩種嗎?重寫自定義 View 的 onDraw() 方法,在裏面加上時間戳,我們也能把 onDraw() 的調用次數當做渲染完成幀數,首尾相減作爲耗時,分子和分母都有了,不是也能得到幀率嗎?

我們通過一張圖來對比下這種方法和上述兩種方式的差異和合理性。

這裏寫圖片描述

上圖是通過 systrace 在華爲 mate9 pro (7.0 系統)真機上獲取的。從 5.0 系統開始,安卓系統使用 UI 線程 + Render 線程共同完成幀渲染。該幀圓圈被標誌爲黃色,表示渲染耗時超過 16.6ms。

對該幀而言,通過 Choreographer 和 gfxinfo 獲取的幀渲染耗時是不相等的。Choreographer 方式得到的幀耗時依然是 16.6ms,而 gfxinfo 則大於 16.6ms。

注意,圖中豎直方向上的 UI 線程和 Render 線程的色塊耗時並不總是對應的。可能由於掉幀現象的出現,導致 UI 線程繪製的幀對應的 Render 線程繪製過程後移。

而對於,藍色部分,由於我們無法準確控制自定義 View onDraw() 方法開始執行的時間,得到的耗時是非常不準確的,而且無法排除這個耗時包括了滑動停頓的時間,更加加大了這種方式的誤差。

UI 線程和 Render 線程共同完成幀繪製和渲染,也就是說,一幀的耗時分佈在兩個線程中,等於二者的耗時之和。但是 Choreographer 方式只統計了 UI 線程中的部分,而忽略了 Render 線程的部分,這樣會導致得到的耗時偏小、幀率偏大。更進一步,既然一幀的繪製和渲染分佈於兩個線程, 那即使一幀的耗時綜合大於 16.6 ms,只要位於 UI 線程部分的耗時不超過 16.6ms,那麼就不影響 UI 線程繪製下一幀,但是會導致屏幕接着顯示上一幀的內容,出現掉幀,而 Choreographer 僅憑幀耗時無法檢測出這種掉幀。

而且,從可行性上考慮,重寫 onDraw() 獲取幀數據的方式並不靠譜。因爲:

  • 對業務代碼侵入性太強;
  • 自定義 View 應該放在視圖樹的哪個層級呢?葉節點?

性能指標

關於幀率和掉幀率,這裏注重闡釋一下。通過這個視頻 why 60 fps ?,我們可以看出,30 fps 和 60 fps 的區別並不是那麼的明顯,如果每一個瞬間頁面的幀率都是 30 fps 其實是感覺不到卡頓的,只是會感覺到滯後感、拖拉感,放佛活在慢鏡頭裏。而“卡頓”,出現在幀率突變的時刻,比如前一段時間都是 60fps 突然降到 30 fps 了,就能明顯感覺卡頓。

幀率可以衡量一個時間段內的的渲染性能,但是比較粗略。比如,在相同的時間內,掉了 500 幀,下面兩種情況的幀率相同,但是用戶體驗卻天壤之別:

  1. 每兩幀掉一幀,即掉幀均勻分佈,每幀的渲染耗時均在 17-32 ms,此時用戶感受到相對流暢的頁面滑動;
  2. 掉幀不均勻,掉幀集中出現在某段時間內,那麼在這段時間內用戶會覺得“ app 卡死了,界面凍住了”,估計多數用戶此時會殺掉 app;

因此,單純通過幀率來衡量性能是不夠嚴謹的,比如 Facebook 就用連續掉 2+ 幀的比例來衡量 fps,Jason Sendros 對此指標合理性的解釋是( 視頻《Road to 60fps》第 14:14 處):

1 frame drop is noticeable if you are staring at something and the rest of the app is buttery smooth. If you start where you are not super buttery smooth 1 frame drop is going to completely unnoticeable.
2 consecutive frame drops is a little bit noticeable and it’s kind of annoying.
3 frame drops gets a bit worse.
4 gets a little irritating.
By 5 frame drops you are not even sure if the app is responding to you when you are doing something for a short period of time and it’s just a really frustrating experience.

除了幀率,還有如下指標用於衡量頁面滑動的流暢程度:

  1. 掉幀率,其實更確切說是“超時率”,即耗時超過 16.6 ms 的幀在所有渲染完成的幀總數的佔比。某一幀“掉了”,並不是這一幀沒有繪製完成被丟掉了或者沒被顯示在屏幕上,而是,雖然沒有“按時”顯示在屏幕上但是還是顯示了。幀率無法衡量掉幀的分佈程度,是密集還是分散。
  2. 出現連續 2+ 掉幀的比例(Facebook) 遭遇掉幀率在 50%+ 的用戶的比例(Slow Rendering,Engineer for High Performance with Tools from Android & Play 25:48)
  3. 遭遇 700ms+ 耗時幀佔比大於 0.1% 的用戶的比例(Frozen Frames,Engineer for High Performance with Tools from Android & Play 25:48)

這裏寫圖片描述

更多好文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章