UI 優(yōu)化系列專題,來聊一聊 Android 渲染相關知識,主要涉及 UI 渲染背景知識、如何優(yōu)化 UI 渲染兩部分內(nèi)容。
UI 優(yōu)化系列專題
- UI 渲染背景知識
《View 繪制流程之 setContentView() 到底做了什么?》
《View 繪制流程之 DecorView 添加至窗口的過程》
《深入 Activity 三部曲(3)View 繪制流程》
《Android 之 LayoutInflater 全面解析》
《關于渲染,你需要了解什么?》
《Android 之 Choreographer 詳細分析》
- 如何優(yōu)化 UI 渲染
《Android 之如何優(yōu)化 UI 渲染(上)》
《Android 之如何優(yōu)化 UI 渲染(下)》
從產(chǎn)品和設計師的角度,他們自然希望應用可以使用豐富的圖形元素、更炫酷的動畫來實現(xiàn)流暢的用戶體驗。但是 Android 系統(tǒng)很有可能無法及時完成這些復雜界面的渲染操作,這個時候就會出現(xiàn)掉幀。也正因如此,我們才需要做 UI 優(yōu)化。UI 優(yōu)化要解決的核心問題是,由渲染性能本身造成用戶感知的卡頓,可以把它理解為卡頓優(yōu)化的一個子集。
工欲善其事,必先利其器,在做 UI 優(yōu)化之前,我們必須先要找到由渲染性能引起卡頓的問題點,有哪些工具可以幫助我們分析和測量呢?今天我們先來聊一聊 UI 渲染的測量方法。
布局分析
在 Android 中,絕大多數(shù)視圖元素都是通過 xml 布局完成的,接下來我們就從布局優(yōu)化開始逐步的排查和分析。
1. Hierarchy Viewer
說到布局優(yōu)化,不得不提 Hierarchy Viewer 這款利器,它是 Android Device Monitor 中內(nèi)置的一種布局性能分析工具,它通過柵格化獲取當前布局界面的顯示基元,并將其轉(zhuǎn)換為屏幕像素的過程。
Tree View:顯示視圖層次結構的樹狀視圖。通過鼠標或底層的縮放控件來拖動和縮放 View 樹。每個節(jié)點都指示它的 View 類名和 ID 名稱。
Tree Overview:應用界面完整視圖層次結構。移動灰色矩形可更改 Tree View 中可見視圖的窗口。
Layout View:顯示布局線框視圖。當前所選視圖的輪廓為紅色,其父視圖為淺紅色。
Hierarchy Viewer 可以測量每個視圖節(jié)點相對于其他同級視圖的性能,因此分析結果中總是會有紅色節(jié)點,紅色節(jié)點并不一定意味著該節(jié)點的布局性能表現(xiàn)不佳。不過它卻是當前視圖層級中表現(xiàn)最差的。
所選節(jié)點的每個子級都有三個圓點,可以是綠色、黃色或紅色。
左側圓點表示渲染管道的繪制過程;
中間圓點表示布局階段;
右邊圓點表示執(zhí)行階段。
這些圓點大致對應繪制流程的測量、布局和繪制階段。圓點的顏色表示在當前層級內(nèi)相對于其他節(jié)點的性能對比。
綠色:表示視圖的渲染速度至少比其他一半的視圖要快;
黃色:表示視圖渲染速度在當前層級處于中間階段;
紅色:表示視圖渲染速度在當前層級處于最慢之一。
需要注意,如果應用的運行速度出乎意料的慢,則紅色節(jié)點可能是有問題的;在同一個層級內(nèi),總有一個最慢的視圖節(jié)點,但是我們只需要確保它是按照我們“預期”的結果即可。那該如何正確理解是符合預期的紅色圓點呢?
查找子節(jié)點中的紅色圓點或僅包含少數(shù)子節(jié)點的視圖容器。
如果一個視圖容器包含許多子視圖和一個紅色圓點,此時需要進一步了解子視圖的渲染情況。
如果一個視圖容器具有紅色測量階段、紅色布局階段和黃色繪制階段,這就屬于比較典型的情況,因為它是所有子視圖的父容器,并且要在所有子視圖完成之后其布局才會完成。
如果具有 20 個以上的視圖容器中,某個子視圖包含紅色繪制階段,則表示存在問題。需要進一步檢查它的繪制操作。
雖然 Hierarchy Viewer 并不能真實反映設備上的實際渲染性能,但是我們可以通過多次分析以了解平均測量結果。總的來看, 它還是能夠幫助我們檢查布局結構中每個視圖模塊的渲染性能,以及查找出冗余或?qū)е滦阅芷款i的視圖節(jié)點。
Layout Inspector
不過,在 Android Studio 3.1 及以后, Hierarchy Viewer 已經(jīng)被棄用,此時 Android 推薦使用 Layout Inspector 來檢查應用的視圖層次結構。
使用 Layout Inspector 可以將應用布局與設計模型進行比較、顯示應用的放大視圖,并在運行時檢查其布局細節(jié)。
View Tree:布局中視圖的層次結構;
Layout Inspector 工具欄:Layout Inspector 包含的工具;
屏幕截圖:設備上顯示的應用布局的屏幕截圖,其中顯示了每個視圖的布局邊界;
Properties Table:選定視圖的布局屬性。
整體來看,Layout Inspector 并沒有太多亮點功能,相反它還取消了 Hierarchy Viewer 中有關布局性能的分析信息。Hierarchy Viewer 接入真機調(diào)試也非常容易,具體你可以參考這里。
2. Use Lint
在布局文件中運行 Lint 工具,以搜索視圖結構中可能潛在的問題,將始終是一個好的習慣!Lint 取代了早期 Layoutopt 工具,并且已經(jīng)默認被集成到 Android Studio。
無論何時編譯項目 Lint 都會自動運行,檢查 Android 源文件是否存在潛在的錯誤,并提供修改建議以及可以直接跳轉(zhuǎn)到問題代碼進行審查。那 Lint 可以幫助我們檢查哪些布局問題呢?
- 使用復合繪圖 — 僅包含一個 ImageView 和 TextView 的 LinearLayout 可以替換為更有效的復合 Drawable,具體可以參考 Compound Drawables。
TextView tv = (TextView) findViewById( R.id.textView );
tv.setCompoundDrawablesWithIntrinsicBounds( 0, R.drawable.ic_launcher, 0, 0 );
合并根視圖 — 如果 FrameLayout 是布局的根視圖,并且不提供背景或填充,那么此時可以使用 merge 標簽來替換它。
無用的子視圖 — 沒有子元素或沒有背景的布局通常可以被刪除(因為它是不可見的),從而獲得更加扁平、有效的布局結構。
無用的父視圖 — 一個沒有子視圖的的布局元素,如果不是 ScrollView 或者跟視圖,也沒有 Background 是可以被移除的。并且讓它的子元素直接移動到父視圖中。從而獲得更加扁平、有效的布局結構。
布局深度 — 布局層級越深展開花費時間就越長,應盡可能保持布局扁平化設計,如 RelativeLayout 或 GridLayout 來提高性能。它們默認最大深度為 10。不過現(xiàn)在 Android 更加推薦使用 ConstraintLayout。
下圖展示了 Lint 工具如何檢查應用源文件,源文件包括:Java、Kotlin 和 XML 文件、圖標以及 ProGuard 配置文件等。有關 Lint 的更多配置你可以參考《使用 Lint 檢查改進您的代碼》
渲染測量
1. Show GPU Overdraw
在 Android 4.2,系統(tǒng)增加了檢測過渡繪制(Overdraw)的工具,通過對應用界面進行顏色編碼來幫助我們識別過渡繪制。具體可以參考《檢查 GPU 渲染速度和繪制過渡》。
當應用界面在同一幀內(nèi)多次繪制同一個像素時,便會發(fā)生過渡繪制。這種可視化會工具可以顯示出應用界面不必要的渲染工作;因為,這樣的繪制任務往往對用戶是不可見的。因此,我們應該盡可能避免過渡繪制的場景。
過渡繪制顏色等級劃分為 5 個階段:
真彩色:沒有發(fā)生過渡繪制;每個像素點僅繪制 1 次;
藍色:過渡繪制 1 次,這部分像素在屏幕上繪制了 2 次;
綠色:過渡繪制 2 次,這部分像素在屏幕上繪制了3 次;
粉色:過渡繪制 3 次,這部分像素在屏幕上繪制了 4 次;
紅色:過渡繪制 4 次或更多次,這部分像素在屏幕上繪制了 5 次以上。
注意,有些過渡繪制可能是不可避免的。在實際開發(fā)過程中,我們應盡可能保證更多的原色或藍色區(qū)域,極少部分的綠色或粉色,最好不要出現(xiàn)紅色。
- 減少布局層級是根絕 Overdraw 最有效的手段
2. Profile GPU Rendering
我們還可以開啟 Profile GPU Rendering 檢查界面的渲染性能。該工具以滾動柱狀圖的形式,直觀地展示了渲染界面每幀所花費的時間(以每幀 16ms 的速度作為對比基準)。
在 Android 6.0(API Level 23)之后,會輸出下面的計算和繪制每個階段的耗時:
在 4.0(API Level 14)和 5.0(API Level 21)之間的 Android 版本具有藍色、紫色、紅色和橙色區(qū)段。低于 4.0 的 Android 版本只有藍色、紅色和橙色區(qū)段。
如果我們把上面的步驟轉(zhuǎn)化為線程模型,可以得到下面的流水線模型。CPU 將數(shù)據(jù)同步(sync)給 GPU 之后,一般不會阻塞等待 GPU 渲染完畢,而是通知結束后就返回。而 RenderThread 承擔了比較多的繪制工作,分擔了主線程很多壓力,提高了 UI 線程的響應速度。
通過上面的一些分析和測量工具,我們可以初步判斷應用 UI 渲染的性能是否達標,例如經(jīng)常出現(xiàn)的掉幀、掉幀主要發(fā)生在哪一個階段、是否存在 Overdraw 等。
數(shù)據(jù)測量
雖然這些圖形化界面工具非常好用,但是它們難以提供準確的測量數(shù)據(jù),那有哪些測量方法可以更精確的實現(xiàn)渲染測量呢?
1. Systrace
在 Android 4.1,系統(tǒng)還新增了 Systrace 性能分析工具。Systrace 默認只能監(jiān)控系統(tǒng)特定調(diào)用的耗時情況,例如跟蹤系統(tǒng)的 I/O 操作、CPU 負載、Surface 渲染、GC 等事件。而且性能開銷非常低。
Systrace 利用了 Linux 的 ftrace 調(diào)試工具,相當于在系統(tǒng)各個關鍵位置都添加了一些性能探針,也就是在代碼里加了一些性能監(jiān)控的埋點。Android 在 ftrace 的基礎上封裝了 atrace,并增加了更多特有的探針,例如 Graphics、Activity Manager、Dalvik VM、System Server 等。
Systrace 會生成一份包含多個部分的 HTML 文件,包括每個進程渲染界面沿時間軸所指明的渲染幀。具體該如何解讀這份報告你可以參考 Systrace 報告。
拿起筆,劃重點了
由于系統(tǒng)預留了 Trace.beginSection 接口來監(jiān)聽應用程序的調(diào)用耗時,我們可以在 Systrace 上增加應用程序的耗時分析:通過編譯時給每個函數(shù)插樁的方式來實現(xiàn),也就是在重要函數(shù)的入口和出口分別增加 Trace.beginSection 和 Trace.endSection。當然出于性能考慮,我們需要過濾大部分指令數(shù)比較少的函數(shù),這樣就實現(xiàn)了在 Systrace 基礎上增加應用程序耗時的監(jiān)控。具體你可以參考為 Systrace 定義自定義事件。
public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Trace.beginSection("MyAdapter.onCreateViewHolder");
MyViewHolder myViewHolder;
try {
myViewHolder = MyViewHolder.newInstance(parent);
} finally {
Trace.endSection();
}
return myViewHolder;
}
}
2. Tracer for OpenGL ES
Tracer for OpenGL ES 也是 Android 4.1 新增加的工具,它可以逐幀、逐函數(shù)的記錄 App 用 OpenGL ES 的繪制過程。它提供了每個 OpenGL 函數(shù)調(diào)用的消耗時間,所以很多時候用來做性能分析。但因為其強大的記錄功能,在分析渲染問題時,當 TraceView、Systrace 都顯得棘手,還找不到問題所在時,此時這個工具就會派上用場了。
Graphics API Debugger
不過,正如前面文章中介紹 Android 渲染框架的演進非常快,在 Android Studio 3.1 之后,Android 推薦使用 Graphics API Debugger (GAPID)來替代 Tracer for OpenGL ES 工具。GAPID 可以說是它的升級版本,不僅支持跨平臺,而且功能更加強大,支持 Vulkan 和回放。
通過上面的幾個工具,我們可以進一步判斷、分析應用 UI 渲染性能數(shù)據(jù),并且能夠找出潛在的渲染瓶頸點。
自動化測試場景
雖然上面這些工具已經(jīng)非常強大,不過線下的測試無論如何也難以復現(xiàn)線上大數(shù)據(jù)用戶場景,此時我們需要一套能夠在產(chǎn)品上線后的自動化測量工具,那有哪些測量方法可以滿足我們的需求呢?
1. gfxinfo
gfx info 可以輸出包含各階段的動畫以及幀相關的性能分析,具體命令如下:
adb shell dumpsys gfxinfo packageName
除了渲染的性能之外,gfxinfo 還可以拿到渲染相關的內(nèi)存和 View Hierarchy 信息。
View Hierarchy:
com.xxx.android.xxx/com.weex.app.DebugActivity/android.view.ViewRootImpl@f80e215
14 views, 11.05kB of display lists
com.xxx.android.xxx/com.weex.app.WeexActivity/android.view.ViewRootImpl@d92a
84 views, 96.78kB of display lists
Total ViewRootImol:2
Total Views: 98
Total DisplayList: 107.82 kB
在 Android 6.0 之后,gfxinfo 命令新增了 framestats 參數(shù),可以拿到最近 120 幀每個繪制階段的耗時信息。
adb shell dumpsys gfxinfo packageName framestats
通過這個命令我們可以實現(xiàn)自動化統(tǒng)計應用的幀率,更進一步還可以實現(xiàn)自定義的 “Profile GPU Rendering” 工具,在出現(xiàn)掉幀的時候,自動統(tǒng)計分析是哪個階段的耗時增長最快,同時給出相應建議。
2. SurfaceFlinger
除了耗時,我們還比較關心渲染使用的內(nèi)存。在前面文章我們有講過,Android 4.1 以后每個 Surface 都會有三個 Graphic Buffer,那如何查看 Graphic Buffer 占用的內(nèi)存,系統(tǒng)是怎么樣管理這部分的內(nèi)存的呢?
你可以通過下面的命令拿到 SurfaceFlinger 相關的信息:
adb shell dumpsys SurfaceFlinger
以我的測試項目為例,應用使用了三個 Graphic Buffer 緩沖區(qū),當前用在顯示的第 0 個 Graphic Buffer,大小是 1080 * 1920。這樣我們可以更好地理解三緩沖機制,而且可以看到這三個 Graphic Buffer 的確在交替使用。
Layer 0x7b5dfb7000 (com.xxx.android.bbt/com.xxx.android.xxx.MainActivity)
// 序號 // 狀態(tài) // 對象 // 大小
[00:0x7be3a490e0] state=ACQUIRED 0x7be3a2eaa0 frame=471 [1080x1920:1088, 1]
[02:0x7be3a499a0] state=FREE 0x7be3a50780 frame=469 [1080x1920:1088, 1]
[01:0x7be3a492a0] state=FREE 0x7b5e883e60 frame=470 [1080x1920:1088, 1]
看下三個 Buffer 分別占用的內(nèi)存:
Allocated buffers:
0x7be1bf6360: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 1 | 0x20001a00
0x7be1bf63c0: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 1 | 0x20001a00
0x7be1bf6420: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 1 | 0x20001a00
這部分的內(nèi)存其實真的不小,特別現(xiàn)在手機分辨率越來越大,而且應用可能還會存在其他 Surface 的情況,例如 SurfaceView 或者 TextureView。
3. Choreographer
幀率,業(yè)界一般都使用 Choreographer 來監(jiān)控應用的幀率。但是需要排除頁面在沒有操作的情況,也就是說只在界面存在繪制的時候才做統(tǒng)計。
那么如何監(jiān)聽界面是否存在繪制行為呢?具體你可以參考《Android 之 ViewTreeObserver 全面解析》。
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
我們經(jīng)常用平均幀率來衡量界面流暢度,但事實上電影的幀率才 24 幀,用戶對于應用的平均幀率是 40 幀還是 50 幀并不一定可以感受出來。對于用戶來說,感覺最明顯的是連續(xù)丟幀情況,Android Vitals 將連續(xù)丟幀超過 700 ms 定義為凍幀,即連續(xù)丟幀超過 42 幀。
因此,我們可以統(tǒng)計更有價值的凍幀率。凍幀率就是計算發(fā)生凍幀時間在所有時間的占比。出現(xiàn)丟幀的時候,我們可以獲取當前的頁面信息、View 信息和操作路徑進行上報,降低二次排查的難度。
另外還可以進一步細化問題,按照 Activity、Fragment 或者某個操作定義場景,通過細化不同場景的平均幀率和凍幀率,進一步細化問題排查的范圍。
最后
Android 渲染框架在快速的演進,可能隨著版本的升級,有些工具也不再適用;工具只是幫助我們排查問題的一種手段,比工具本省更重要的是了解和學習它們背后的工作原理,這對我們的成長會有很大的幫助。
日常開發(fā)中我們不能只滿足于完成需求就可以了,在實現(xiàn)的同時還要多去思考例如內(nèi)存、卡頓、渲染等這些影響性能的點,日積月累我們的進步自然也會更快一些。
相信你也肯定有很多好的渲染分析或其他性能優(yōu)化的思路和方法,歡迎大家留言分享你的性能優(yōu)化“必殺技”。文中如有不妥或有更好的分析結果,歡迎您的指正。
文章如果對你有幫助,就請留個贊吧!
擴展閱讀
關于 UI 渲染,你需要了解什么?
Android 之你真的了解 View.post() 原理嗎?
深入 Activity 三部曲(3)之 View 繪制流程
...
其他系列專題
Android 存儲優(yōu)化系列專題
Android 之不要濫用 SharedPreferences
Android 存儲選項之 SQLite 優(yōu)化那些事兒
Android 對象序列化之追求完美的 Serial
...