UI優化

UI渲染基礎

1、屏幕與適配


通過dp和自適應布局可以基本解決屏幕碎片化的問題,這也是Android推薦使用的屏幕兼容性適配方案,但它存在兩個比較大的問題:

  • 不一致行,由于dpi與實際的ppi的差異性,導致在相同分辨率的手機上,控件的實際大小會有所不同
  • 效率,設計師設計稿都是以px為單位的,開發人員為了UI適配,需要手動通過百分百估算出dp值
    除了直接dp適配之外,目前比較常用的適配方案有兩種:
  • 限制符適配方案,主要有寬高限定符與smallestWidth限定符適配方案
  • 今日頭條適配方案,通過反射修正系統的density值

2、CPU與GPU

UI組件在繪制到屏幕之前,都需要經過Rasterization(柵格化)操作,而柵格化操作有是一個非常耗時的操作,GPU也就是圖形處理器,它主要用于處理圖形運算,可以幫助我們加快柵格化操作。


3、OpenGL和Vulkan

硬件繪制,我們通過調用OpenGL ES接口利用GPU完成繪制,OpenGL是一個跨平臺的圖形API,它為2D/3D圖形處理硬件制定了標準軟件接口,而OpenGL ES是OpenGL的子集,專為嵌入式設備設計。



Android 7.0把OpenGL ES升級到最新的3.2版本同時,還添加了對Vulkan的支持。Vulkan是用于高性能的3D圖形的低開銷、跨平臺API。在改善功耗、多核優化提升繪圖調用上有著非常明顯的優勢。

Android渲染


Android各組件的作用:

  • 畫筆:Skia或者OpenGL,我們用Skia畫筆繪制2D圖形,也可以用OpenGL來繪制2D/3D圖形
  • 畫紙:Surface,在Android 中Window是View的容器,每個窗口都會關聯一個Surface,而WindowManager則負責管理這些窗口,并把它們的數據傳遞給SurfaceFlinger
  • 畫板:Graphic Buffer,用于應用程序圖形的繪制,在Android 4.1之前是雙緩沖機制,在Android 4.1之后采用三緩沖機制
  • 顯示:SurfaceFlinger,它將WindowManager提供的所有Surface,通過硬件合成器Hardware Composer合成并輸出到顯示屏。

1、Android 4.0開啟硬件加速


在Android3.0之前,或者沒有啟用硬件加速時,系統會使軟件方式渲染UI,整個流程如上圖所示:

  • Surface,每個View都由某一個窗口管理,而每個窗口都關聯有一個Surface
  • Canvas,通過Surface的lock函數獲得一個Canvas,Canvas可以簡單理解為Skia底層接口的封裝
  • Graphic Buffer, SurfaceFlinger會幫我們托管一個BufferQueue,我們沖BufferQueue中拿到Graphic Buffer,然后通過Canvas以及Skia將繪制內容柵格化到上面
  • SurfaceFlinger, 通過Swap Buffer 把Front Graphic Buffer 的內容交給SurfaceFlinger,最后硬件合成器Hardware Composer合成并輸出到顯示屏。
    Android 3.0開始支持硬件加速,到Android 4.0 時默認開啟硬件加速



    硬件加速繪制與軟件繪制整個流程差異非常大,最核心就是我們通過GPU完成Graphic Buffer的內容繪制。此外硬件繪制還引入了一個DisplayList的概念,每個View內部都有一個DisplayList,當某個View需要重繪時,將它標記會Dirty。
    當需要重繪時,僅僅只需要重繪一個View的DisplayList,而不是像軟件繪制那樣需要向上遞歸。這樣可以大大減少繪圖的操作數量,提高渲染效率。


2、Android 4.1:Project Butter

Project Butter主要包含兩個組成部分:一個是VSYNC,一個是Triple Buffering。

VSYNC信號

VSYNC類似于時鐘中斷,每收到VSYNC中斷,CPU會立即準備Buffer數據,由于大部分顯示設備刷新頻率都是60HZ(一秒刷新60次),也是一幀數據的準備工作要在16ms內完成。



這樣應用總是在VSYNC邊界上開始繪制,而SurfaceFlinger總是VSYNC邊界上進行合成,這樣可以消除卡頓,并提升圖形的視覺表現。

三級緩沖機制Triple Buffering

在Android 4.1之前都使用雙緩沖機制,不同的View或者Activity它們都會共用一個Window,也就是共用一個Surface。而每個Surface都會有一個BufferQueue緩存機制,但是這個隊列會由SurfaceFlinger管理,通過匿名共享內存機制與App應用成交互。



整個流程如下:

  • 每個Surface對應的BufferQueue內部都有兩個Graphic Buffer,一個用于繪制一個用于顯示。會把內容先繪制到離屏緩沖區(OffScreen Buffer),在需要顯示時,才把離屏緩沖區的內容通過Swap Buffer復制到Front Graphic Buffer中
  • 這樣SurfaceFlinger就拿到了某個Surface最終要顯示的內容,但是同一時間可能會有多個Surface,這里可能是不同應用的Surface,也可能是同一個應用里面類似SurfaceView和TextureView,他們都會有自己單獨的Surface
  • 這個時候SurfaceFlinger把所有的Surface要顯示的內容統一交給Hardware Composer,它會根據位置、Z-Order順序等信息合成為最終顯示的內容,而這個內容交給系統的幀緩沖區Frame Buffer來顯示(Frame Buffer是非常底層的,可以理解為屏幕顯示的抽象)。
    如果只有兩個Graphic Buffer緩沖區A和B,如果CPU/GPU繪制時間過程,超過了一個VSYNC信號周期,因為緩沖區B中的數據還沒準備好,所以只能繼續顯示A緩沖區的內容,這樣緩沖區A和B都分別被顯示設備和GPU占用,CPU無法準備下一幀的數據。



    如果再提供一個緩沖區,CPU、GPU和顯示設備都能各自使用各自的緩沖區工作,互不應用。簡單來說,三緩沖機制就是在雙緩沖機制的基礎上增加了一個Graphic Buffer,這樣可以最大限度的利用空閑時間,帶來的壞處是多使用了一個Graphic Buffer所占用的內存。


數據測量

Android 4.1新增了Systrace性能數據采樣和分析工具,Tracer for OpenGL ES也是Android 4.1新增的工具,可逐幀、逐函數的記錄App用OpenGL ES的繪制過程。它提供了每個OpenGL函數調用的消耗時間,所以很多時候用來做性能分析。在Android 4.2系統增加了檢測過度繪制工具。

3、Android 5.0:RenderThread

經過Project Buffer黃油計劃之后,Android的渲染性能有了很大的改善,但是有一個問題,雖然我們利用了GPU的圖形高性能運算,但是從計算DisplayList,到通過GPU繪制到Frame Buffer,整個計算和繪制都在UI主線程中完成。



UI主線程任務過于繁重,如果整個渲染過比較耗時,可能會造成用戶無法響應的操作,進而出現卡頓。GPU對圖形的繪制渲染能力更勝一籌,如果使用GPU并在不同的線程繪制渲染圖形,那么整個流程會更加順暢。
所以Android 5.0引入了兩個比較大的改變。一是RenderNode,它對DisplayList及一些View的顯示屬性做了進一步的封裝;另一個是RenderThread,所欲的GL命令執行都放到這個線程上,渲染線程在RenderNode中存有渲染幀的所有信息,可以做一些屬性動畫,這樣即便主線程有耗時的操作也可以保證動畫流程。
我們可以開啟Profile GPU Rendering檢查,在Android 6.0之后,會輸出下面的計算和繪制每個階段的耗時:



如果將上面的步驟轉化為線程模型,可以得到下面的流水線模型。CPU將數據同步(sync)給GPU之后,一般不會阻塞等待GPU渲染完畢,而是通知結束后返回,而RenderThread承擔了比較多的繪制工作,分擔了主線程很多壓力,提高了UI線程的響應速度。

UI渲染測量

  • 測試工具:Profile GPU Rendering 和Show GPU Overdraw
  • 問題定位工具:Systrace和Tracer for OpenGL ES
    在Android 3.1之后,推薦使用Graphic API Debugger(GAPID)來代替Tracer for OpenGL ES工具,GAPID可以說是升級版,它不僅可以跨平臺,而且功能更加強大,支持Vulkan與回放。


1、gfxinfo

gfxinfo可以輸出包含各階段發生的動畫及幀相關的性能信息,具體命令如下:

adb shell dumpsys gfxinfo 包名

除了渲染性能之外,gfxinfo還可以拿到渲染相關的內存和View hierachy信息,在Android 6.0之后,gfxinfo命令增加了framestats參數,可以拿到最近120幀每個繪制階段的耗時信息。通過這個命令可以實現自動化統計應用的幀率,更進一步還可以實現自定義的“Profile GPU Rendering”工具。

adb shell dumpsys gfxinfo 包名 framestats

2、SurfaceFlinger

關于渲染使用的內存情況,可以使用下面的命令拿到SurfaceFlinger相關的信息:

adb shell dumpsys SurfaceFlinger

UI優化的常用手段


要求所有的渲染操作都必須在16ms(=1000ms / 60fps)內完成,UI優化就是拆解熏染的各個階段的耗時,找到瓶頸點,再加以優化。

1、盡量使用硬件加速

硬件加速繪制的性能是遠大于軟件繪制,所以要盡可能的采用硬件加速。有些情況不能采用硬件加速,因為硬件加速不支持所有的Canvas API,具體API兼容列表,查看drawing-support,如果使用了不支持的API,系統就需要通過CPU軟件模擬繪制,這也是漸變、磨砂、圓角等效果渲染性能比較低的原因。
SVG也是一個非常典型的例子,SVG很多指令硬件加速都不支持,但可以提前將SVG轉換成Bitmap保存起來,這樣系統就可以更好的使用硬件加速會繪制,同理對于圓角、漸變等場景,也可以改為Bitmap實現。

2、Create View優化

View的創建是在主線程完成的,對于一些負責的界面,耗時會增加,根據View的創建流程有如下優化點:

使用代碼創建

XML進行UI編寫十分方便且提供可視化預覽,但是效率方面大打折扣,建議在對性能要求非常高,且修改不頻繁的頁面采用代碼創建的方式。

異步創建

如果再線程提前創建View,會報如下的錯誤:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() at android.os.Handler.(Handler.java:121)

解決方案是先將線程的Looper的MessageQueue替換成UI線程Looper的Queue,最后在創建完View后把線程的Looper恢復成原來的。


View復用

正常來說,View會隨著Activity的銷毀而同時銷毀,ListView、RecycleView通過View的緩存和重用大大提升渲染性能。因此可以實現一套可以在不同Activity或Fragment使用的View緩存機制。但是這里需要保證所有進入緩存的View不會保留之前的狀態。


3、measure/layout優化

  • 減少UI布局層次,盡量扁平化,使用<ViewStub><Merg>等優化
  • 優化layout開銷,盡量不使用RelativeLayout或者基于weighted LinearLayout,他們layout的開銷非常巨大,建議使用ConstrainLayout替代。
  • 背景優化,盡量不要重復設置背景
    PrecomputedText提供了接口,可以異步進行measure和layout,不必再主線程中執行。

UI優化其他手段

1、Litho:異步布局

Litho是Facebook開源的聲明式Android UI渲染框架,本身非常強大,內部做了很多優化。

  • 異步布局
    一般來說Android所有的控件繪制都要遵守measure -> layout -> draw的流水線。


Litho把measure和layout都放到了后臺線程,只留下了必須要再主線程完成的draw,大大降低了UI線程的負載。


  • 界面扁平化
    Litho使用自有的布局引擎(Yoga),在布局階段可以檢測不必要的層級,減少ViewGroups,來實現UI扁平化。
  • 優化RecycleView
    Litho優化RecycleView中UI組件的緩存和回收方法,原生RecycleView或者ListView是按照viewType來進行緩存和回收,但是如果一個RecycleView/ListView中出現過多的viewType,會使緩存形同虛設,Litho是按照text、image和video獨立回收的,可以提高緩存命中率、降低內存使用率、提高滾動幀率。


2、Flutter:自己的布局+渲染引擎

Flutter是谷歌推出的開源移動應用開發框架,可發著可以通過Dart語言開發App,一套代碼同時運行在iOS和Android平臺。在Android上Flutter完全沒有基于系統的渲染引擎,而是把Skia引擎直接集成到App中,并且直接使用了Dart虛擬機。



開發Flutter應用總的來說簡化了線程模型,框架給我們抽象出各司其職的Runner,包括UI、GPU、I/O、Platform Runner。Android平臺上面沒一個引擎實例啟動的時候都會為UI Runner、GPU Runner、I/O Runner各自創建一個新的線程,所有Engine實例共享同一個Platform Runner和線程。

  • 首先UI Runner會執行root isolate(可以簡單理解為main函數,isolate是Dart虛擬機中一種執行并發代碼實現,Dart虛擬機實現了Actor的并發模型,與大名鼎鼎的Erlang使用了類似的并發模型。)
  • Flutter 引擎得到通知后,會告知系統我們要同步VSYNC
  • 得到GPU的VSYNC信號后,對UI Widgets進行Layout并生成一個Layer Tree
  • 然后Layer Tree會交給GPU Runner進行合成和柵格化
  • GPU Runner使用Skia庫繪制相關圖形



    Flutter也采用了類似Litho、React屬性不可變,單向數據流的方案,這樣做的好處是將視圖與數據分開。

3、RenderThread與RenderScript

在Android 5.0系統增加了RenderThread對于ViewPropertyAnimator和CircularReveal動畫,可以使用RenderThread實現動畫的異步渲染。圖片的變換涉及大量的計算任務,可以通過RenderScript提高性能,它是Android操作系統提供的一套API,基于異構計算思想,專門用于密集計算。RenderScript提供了三個基本工具:一個硬件無關的通用計算API;一個類似于CUDA、OpenGL和GLSL的計算API;一個類C99的腳本語言,語序開發者以較少的代碼實現功能復雜且性能優越的應用程序。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容