系統(tǒng)級別的流暢度優(yōu)化
流暢度應(yīng)該是終端用戶感知最明顯的性能指標(biāo)了频鉴,提升流暢度是提升用戶體驗性價比最高的方式之一,我們先來看看在系統(tǒng)層面上Google為了優(yōu)化流暢度做了哪些努力
Vsync(垂直同步)
垂直同步是一個游戲中很常見的概念列吼,它的出現(xiàn)是為了解決如下圖的畫面撕裂的問題
究其原因是屏幕的刷新并不是瞬時完成的拨脉,而GPU產(chǎn)生一幀新畫面的速度和屏幕刷新速度不同步,當(dāng)GPU速度又大于顯示器的刷新速度,在顯示器從上到下掃描顯示的過程中圖像緩沖就被更新了逻翁,顯示器并不知道這個變化還是繼續(xù)掃描,就產(chǎn)生了畫面撕裂
android 4.1的黃油計劃引入垂直同步之后捡鱼,只有在接收到Vsync信號八回,系統(tǒng)才會讓CPU/GPU開始下一幀的渲染工作,即每個屏幕刷新周期之間最多只會產(chǎn)生一幀畫面,以此避免畫面撕裂
一旦收到VSync信號缠诅,立刻就開始執(zhí)行下一幀的繪制工作溶浴。這樣也可以大大降低Jank出現(xiàn)的概率。只需要保證渲染一幀畫面的時間在1/60s(16ms)就行了
Triple Buffer
先看看雙緩沖的模型
兩個緩存區(qū)分別為 Back Buffer 和 Frame Buffer管引。GPU 向 Back Buffer 中寫數(shù)據(jù)士败,屏幕從 Frame Buffer 中讀數(shù)據(jù)。VSync 信號負(fù)責(zé)調(diào)度從 Back Buffer 到 Frame Buffer 的復(fù)制操作褥伴,可認(rèn)為該復(fù)制操作在瞬間完成(只是交換了內(nèi)存地址)谅将。
如果所有渲染操作都在16ms之內(nèi)完成,雙重緩沖可以很好的工作重慢,但是渲染耗時超過16ms呢
第一個B畫面的渲染超過了16ms戏自,因為此時B畫面占據(jù)了Back Buffer,所以當(dāng)接收到下一幀的VSync信號時系統(tǒng)沒有開始渲染工作伤锚,導(dǎo)致jank的發(fā)生
而三重緩沖增加了一個Back Buffer
在接收到VSync信號擅笔,B畫面還在渲染,因為CPU已經(jīng)空閑了屯援,而且有另一塊緩沖區(qū)猛们,所以同時開始了下一幀的渲染工作
三重緩沖可以更充分的利用CPU/GPU提升畫面顯示的流暢度
- 為什么不繼續(xù)增加緩沖區(qū)來提升流暢度呢?
硬件加速
硬件加速就是依賴GPU實現(xiàn)圖形繪制加速狞洋。
可以看出GPU的ALU(算術(shù)邏輯單元)比CPU多的多弯淘,而圖形處理和柵格化操作實際就是大量的數(shù)學(xué)計算,所以用GPU去渲染圖形比CPU快的多
android 3.0引入了硬件加速吉懊,android4.0以后默認(rèn)開啟了硬件加速
當(dāng)啟動硬件加速后庐橙,Android 使用 “DisplayList” 組件進(jìn)行繪制而非直接使用 CPU 繪制每一幀。DisplayList 是一系列繪制操作的記錄借嗽,抽象為 RenderNode 類态鳖。
這樣間接的進(jìn)行繪制操作的優(yōu)點很多:
- DisplayList 創(chuàng)建時并不是真正的繪制,只是記錄了繪制的操作恶导,所以對DisplayList的修改開銷比較小浆竭。
- 特定的View屬性變化(如 translation, scale 等)只是修改了View對應(yīng)的DisplayList的屬性惨寿,而不需要重新生成新的DisplayList邦泄。
- 當(dāng)知曉了所有繪制操作后,可以針對其進(jìn)行優(yōu)化:例如裂垦,所有的文本可以一起進(jìn)行繪制一次顺囊。
- 可以將對 DisplayList 的處理轉(zhuǎn)移至另一個線程(非 UI 線程)。
RenderThread
RenderThread是Android 5.0引入的功能蕉拢。
渲染工作的真正執(zhí)行者是 GPU特碳,而 GPU是不知道什么是動畫的:執(zhí)行動畫的唯一途徑便是將每一幀的不同繪制操作分發(fā)給 GPU诚亚,但該邏輯本身不能在 GPU 上執(zhí)行。
而如果在 UI 線程執(zhí)行該操作测萎,任意的重操作都將阻塞新的繪制指令及時分發(fā),動畫就很容易出現(xiàn)延遲和卡頓届巩。
添加一個RenderThread專門用來處理渲染的相關(guān)操作硅瞧,UI線程只管計算生成一個DisplayList,剩下的渲染相關(guān)的事情就交給RenderThread恕汇,這樣減輕了UI線程的負(fù)擔(dān)腕唧,也提升了動畫的流暢度
- Android 6.0不同場景的軟/硬件繪制分析
渲染場景 | 純軟件繪制 | 硬件加速 | 加速效果分析 |
---|---|---|---|
頁面初始化 | 繪制所有View | 創(chuàng)建所有DisplayList | GPU分擔(dān)了復(fù)雜計算任務(wù) |
在一個復(fù)雜頁面調(diào)用背景透明TextView的setText(),且調(diào)用后其尺寸位置不變 | 重繪臟區(qū)所有View | TextView及每一級父View重建DisplayList | 重疊的兄弟節(jié)點不需CPU重繪瘾英,GPU會自行處理 |
TextView逐幀播放Alpha / Translation / Scale動畫 | 每幀都要重繪臟區(qū)所有View | 除第一幀同場景2枣接,之后每幀只更新TextView對應(yīng)RenderNode的屬性 | 刷新一幀性能極大提高,動畫流暢度提高 |
修改TextView透明度 | 重繪臟區(qū)所有View | 直接調(diào)用RenderNode.setAlpha()更新 | 加速前需全頁面遍歷缺谴,并重繪很多View但惶;加速后只觸發(fā)DecorView.updateDisplayListIfDirty,不再往下遍歷湿蛔,CPU執(zhí)行時間可忽略不計 |
小結(jié)
系統(tǒng)層面對于UI流暢度的優(yōu)化措施
- VSync(解決畫面撕裂)
- 三重緩沖(提升CPU/GPU利用率)
- 硬件加速(使用GPU加速畫面渲染)
- RenderThread(減輕UI線程負(fù)擔(dān))
收集流暢度相關(guān)信息
google為了畫面的流暢度可謂是用心良苦膀曾,作為一個有追求的開發(fā)者,我們當(dāng)然也要朝著如絲般順滑努力阳啥,首先先從數(shù)據(jù)收集開始
開啟GPU呈現(xiàn)模式分析
這個就是傳說中的玄學(xué)曲線了添谊,可以通過它可視化的直觀掌握當(dāng)前界面是否流暢
綠線是16ms的分界線,每一個豎條代表渲染一幀的耗時察迟,要保證流暢理論上需要每一條都在綠線之下
有幾個關(guān)鍵點需要注意一下
- 表中的顏色跟實際的真機(jī)顏色有一些差別
- 雖然叫GPU呈現(xiàn)模式分析斩狱,但是表中的所有階段都發(fā)生在CPU中
優(yōu)點:
- 實時
- 直觀
缺點:
- 無法量化
gfxinfo
android 6.0以上設(shè)備使用adb shell dumpsys gfxinfo <PACKAGE_NAME>
可以獲取到Aggregate frame stats
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 //UI線程任務(wù)過重造成的超時
Number Slow bitmap uploads: 1542 //加載bitmap導(dǎo)致的超時
Number Slow draw: 23342 //繪制太慢導(dǎo)致的超時
使用adb shell dumpsys gfxinfo <PACKAGE_NAME> reset
可以重置數(shù)據(jù),結(jié)束進(jìn)程不會重置Aggregate frame stats
使用adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
可以獲取上120幀的詳細(xì)耗時,這個數(shù)據(jù)跟GPU呈現(xiàn)模式的條形圖是對應(yīng)的
數(shù)據(jù)的單位是納秒扎瓶,通過統(tǒng)計計算可以獲得每一幀的各階段耗時
具體每一列數(shù)據(jù)代表什么可以看下面的鏈接
Framestats data format
優(yōu)點:
- 數(shù)據(jù)詳細(xì)
- 有整體的統(tǒng)計
缺點:
- 不是實時數(shù)據(jù)
- 只有120幀
- framestats數(shù)據(jù)需要進(jìn)一步處理
OnFrameMetricsAvailableListener
從 7.0(API 24)開始所踊,安卓 SDK 新增 OnFrameMetricsAvailableListener 接口用于提供幀繪制各階段的耗時燥透,數(shù)據(jù)源與 GPU Profile 相同读处。
public void startFrameMetrics(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final String activityName = getClass().getSimpleName();
listener = new Window.OnFrameMetricsAvailableListener() {
private int allFrames = 0;
private int jankyFrames = 0;
@Override
public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics,
int dropCountSinceLastInvocation) {
FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics);
allFrames++;
float totalDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.TOTAL_DURATION));
if (totalDurationMs > 17) {
jankyFrames++;
String msg = String.format("Janky frame detected on %s with total duration: %.2fms\n",
activityName, totalDurationMs);
float layoutMeasureDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.LAYOUT_MEASURE_DURATION));
float drawDurationMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.DRAW_DURATION));
float gpuCommandMs = (float) (0.000001 * frameMetricsCopy.getMetric(
FrameMetrics.COMMAND_ISSUE_DURATION));
float othersMs = totalDurationMs - layoutMeasureDurationMs - drawDurationMs - gpuCommandMs;
float jankyPercent = (float) jankyFrames / allFrames * 100;
msg += String.format("Layout/measure: %.2fms, draw:%.2fms, gpuCommand:%.2fms others:%.2fms\n",
layoutMeasureDurationMs, drawDurationMs, gpuCommandMs, othersMs);
msg += "Janky frames: " + jankyFrames + "/" + allFrames + "(" + jankyPercent + "%)"
+ dropCountSinceLastInvocation;
Log.e("FrameMetrics", msg);
}
}
};
getWindow().addOnFrameMetricsAvailableListener(listener, new Handler());
} else {
Log.w("FrameMetrics", "FrameMetrics can work only with Android SDK 24 (Nougat) and higher");
}
}
FrameMetrics中包含了渲染一幀各個階段的耗時數(shù)據(jù)
優(yōu)點:
- 實時
- 數(shù)據(jù)全面
- 直接給出了每個階段的耗時,不用再算一遍了
缺點:
- 只支持7.0及以上系統(tǒng)
Choreographer.FrameCallback
這種檢測流暢度的方法起源于FaceBook的一次關(guān)于UI流暢度的技術(shù)分享The Road to 60FPS
Choreographer是一個接收VSync信號并分發(fā)的組件桥氏,Choreographer 收到通知依次處理 Input乍赫、Animation瓣蛀、Draw,這三個過程都是通過 FrameCallback 回調(diào)的方式完成的雷厂。
通過Choreographer.FrameCallback
可以獲取到VSync信號開始被處理的時間戳惋增,減去上一個時間戳,可以近似為上一幀的渲染耗時(只計算了UI線程的耗時改鲫,計算不到渲染線程和GPU耗時)
public void postFrameCallback(View view) {
lastTime = System.nanoTime();
Choreographer.getInstance().postFrameCallback(this);
}
@Override
public void doFrame(long frameTimeNanos) {
//每個FrameCallback都只會回調(diào)一次诈皿,所以需要在回調(diào)中注冊下一幀VSync信號的回調(diào)
Choreographer.getInstance().postFrameCallback(this);
long jitterNanos = frameTimeNanos - lastTime;
if (jitterNanos > FRAME_INTERVAL_NANOS) {
Log.i(TAG,
"doFrame: lastTime:" + lastTime + " frameTimeNanos:" + frameTimeNanos
+ " frame:" + jitterNanos);
lastTime = frameTimeNanos;
}
Choreographer.FrameCallback
和之前的gfxinfo的數(shù)據(jù)有一些不同
- Choreographer的回調(diào)是基于VSync信號的林束,而gfxinfo的數(shù)據(jù)是基于每一幀的渲染。
- 當(dāng)畫面沒有發(fā)生變化稽亏,畫面是不需要重新渲染的壶冒,此時在GPU呈現(xiàn)模式上的條形圖也不會移動,framestats也不會記錄截歉。
- 而VSync信號總是會發(fā)出來胖腾,所以
Choreographer.FrameCallback
在畫面沒有重新渲染時也會被回調(diào)到。
優(yōu)點:
- 實時
- 基于VSync信號
缺點:
- 小于VSync信號間隔的渲染時間不知道精確值
- 數(shù)據(jù)不夠全面瘪松,不知道每個階段的具體耗時
- 對VSync信號的響應(yīng)只依賴于UI線程空閑與否咸作,渲染線程和GPU的阻塞無法判斷
adb shell dumpsys SurfaceFlinger --latency
這個是網(wǎng)上比較多介紹的獲取數(shù)據(jù)計算fps的方式,但是自己試驗獲取不到有效數(shù)據(jù)宵睦,所以計算fps只能通過gfxinfo中獲取的數(shù)據(jù)了
小結(jié)
流暢度相關(guān)數(shù)據(jù)的收集
- 每一幀的渲染時間
- GPU呈現(xiàn)模式(直觀记罚,無法量化)
- adb shell gfxinfo(可以獲取到總的統(tǒng)計數(shù)據(jù),和最近120幀的詳細(xì)渲染數(shù)據(jù))
- VSync信號被響應(yīng)的時間
- Choreographer(無法精確計算每一幀的耗時)
流暢度的性能指標(biāo)
通過上面的介紹壳嚎,我們收集到了兩種與流暢度相關(guān)的核心數(shù)據(jù)
- 渲染每一幀的耗時
- VSync信號的響應(yīng)時間點
那么這些數(shù)據(jù)如何和流暢度對應(yīng)起來呢桐智?
渲染超時的幀數(shù)量
最直觀的數(shù)據(jù)就是每一幀的渲染耗時了,當(dāng)一幀耗時大于1/60s烟馅,即使只超過1ms酵使,這一幀依然會錯過一個VSync,到下一個VSync信號產(chǎn)生時才能顯示到屏幕上焙糟】谟妫看上去這個值是和流暢度緊密相關(guān)的,但是會不會有什么問題呢穿撮?
先來看一個極端情況
這里每一幀渲染都是超過16ms的缺脉,但是因為三重緩沖和RenderThread的存在,這里是有60fps的悦穿,只不過顯示出現(xiàn)了1/30s的延遲攻礼,并不會讓用戶察覺有什么異樣。
所以單純用單位時間渲染超時的幀數(shù)量來衡量流暢度其實并不太合理栗柒,甚至于google做的那些底層優(yōu)化就是為了讓我們的應(yīng)用在渲染超過16ms時依然有良好的流暢度表現(xiàn)礁扮。
幀率
FPS這是最常用的衡量畫面流暢度的指標(biāo)。
不過在Android系統(tǒng)中用FPS用fps來衡量流暢度卻有些缺陷和不便
- 首先如果畫面沒有變化瞬沦,其實是沒有畫面刷新的太伊,此時FPS為0,但是這種場景下并沒有發(fā)生卡頓逛钻。
- 第二在android 7.0以下的系統(tǒng)中只能通過adb shell 來獲取120幀的數(shù)據(jù)僚焦,限制比較大
- 第三framestats數(shù)據(jù)雖然全面,但是計算比較復(fù)雜
- 有臟數(shù)據(jù)(flags不為0曙痘,測試這樣的數(shù)據(jù)不多芳悲,影響有限立肘,可以直接剔除)
- 兩幀之間可能有重疊(三重緩沖和RenderThread)
- 兩幀之間可能有間隔(畫面不需要更新)
- 每一幀的時間不確定,按固定幀數(shù)計算FPS誤差比較大名扛,按單位時間計算需要自己處理時間周期
丟幀與流暢度
首先先說明下什么是丟幀谅年,理論上屏幕的刷新率是60hz,加入垂直同步機(jī)制之后肮韧,每秒渲染的畫面上限就是60融蹂,因為某一個VSync信號產(chǎn)生時因為UI線程卡頓或者圖像緩沖全部被占據(jù)等情況導(dǎo)致這一個VSync沒有被響應(yīng),這種情況就是丟幀惹苗。而在畫面沒有更新的情況下沒有新的幀需要渲染殿较,這種情況并不是丟幀耸峭。
注意一下丟幀與渲染超時的區(qū)別
- 渲染超時出現(xiàn)時桩蓉,這一幀會被顯示在屏幕上,不過會有延遲劳闹;而丟幀發(fā)生時這個周期的UI狀態(tài)不會被顯示到屏幕上
- 丟幀發(fā)生時一定存在渲染超時院究;而因為RenderThread和三重緩沖機(jī)制的存在,發(fā)生了渲染超時也不一定就會造成丟幀
因為丟幀的計算實際是依賴于對VSync信號的響應(yīng)本涕,自然得用到Choreographer.FrameCallback
public void postFrameCallback(View view) {
lastTime = System.nanoTime();
Choreographer.getInstance().postFrameCallback(this);
disposable = Flowable.interval(200, TimeUnit.MILLISECONDS)
.map(new Function<Long, Integer>() {
@Override
public Integer apply(Long aLong) {
Log.i(TAG, "apply: " + aLong);
int sm = (frameCount - lastFrameCount) * 1000 / 200;
lastFrameCount = frameCount;
return sm;
}
})
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer sm) throws Exception {
StringBuilder builder = new StringBuilder();
builder.append("Smoothness :").append(sm);
for (int i = 0; i < skipCount.length; i++) {
if (i == 0) {
builder.append(" normal: ");
} else {
builder.append(" skip ").append(i).append(" frames: ");
}
builder.append(skipCount[i]);
}
Log.i(TAG, builder.toString());
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
}
});
}
@Override
public void doFrame(long frameTimeNanos) {
Choreographer.getInstance().postFrameCallback(this);
long jitterNanos = frameTimeNanos - lastTime;
frameCount++;
//index表示兩個VSync信號之間被忽略的信號數(shù)量业汰,即丟幀
//FRAME_INTERVAL_NANOS取了17ms,因為取1/60s轉(zhuǎn)換成納秒進(jìn)行比較的話數(shù)值太接近了菩颖,稍有誤差都會比較大的影響結(jié)果
int index = (int) (jitterNanos / FRAME_INTERVAL_NANOS);
if (index > 7) {
index = 7;
}
skipCount[index]++;
lastTime = frameTimeNanos;
}
這里不僅計算了每秒響應(yīng)的VSync信號數(shù)量作為流暢度(實時數(shù)據(jù))样漆,還記錄了不同連續(xù)丟幀發(fā)生的次數(shù)(可以制定多個維度的數(shù)據(jù)上報)
- 丟幀超過50%
- 連續(xù)丟幀2+超過30%
- 丟幀18+表示發(fā)生了300ms以上的卡頓,說明有場景會造成嚴(yán)重卡頓
...
小結(jié)
綜合兼容性晦闰,數(shù)據(jù)與實際場景的契合程度還有計算復(fù)雜度放祟,使用Choreographer統(tǒng)計VSync信號是更為合理的選擇
To Be Continue
本來準(zhǔn)備一篇文章搞定的,但是寫著寫著發(fā)現(xiàn)內(nèi)容越來越多呻右,拆成兩篇感覺更清晰一些跪妥,下一篇會介紹如何去優(yōu)化UI流暢度。
參考資料
Getting To Know Android 4.1, Part 3: Project Butter - How It Works And What It Added
Triple Buffering: Why We Love It
理解 RenderThread
Android硬件加速(二)-RenderThread與OpenGL GPU渲染
Android GPU呈現(xiàn)模式原理及卡頓掉幀淺析
Test UI performance
Choreographer 解析