[Android] UI 性能優化筆記

在 Android 開發過程中不免面臨一個把應用做出來,再到把它做成牛逼的應用的過程,其中非常直觀的一點就是應用 UI 的流暢度。
這裏對一些性能相關的知識進行了小結~

一、UI卡頓的原因
首先,我們評價UI的時候經常會遇到這幾個說法:

1.“這動畫30幀都不到,卡成狗”
2.“這幀率明顯都到50多了怎麼還是卡卡的感覺”
3.“拖動的時候感覺在抖”

這裏其實有兩個問題:
A1. 平均幀率不足
A2. 平均幀率上去了,但是掉幀

幀率不足很好理解,掉幀的場景大概是這樣的
幀率對比

每一列色塊表示一幀的繪製時間,時間越久色塊越寬,那麼可以發現,下面一列幀率達到40的動畫在中間有一段漏了一幀(可能這個時候CPU阻塞了沒空畫圖),在後面有一段繪製特別長(可能在繪製一個很耗時的東西)。那麼整體表現出來的就是下面的動畫在播放過程中會明顯卡兩次,性能表現上還不如上面FPS30的動畫。

小結:
所以當評價動畫時,會發現幀率高是動畫流暢的必要條件,
但還不是充分條件,
平均幀率 決定了動畫體驗的 上限
但是卡頓感往往是最低幀率決定的,一個80ms的卡頓就會毀了你看似達到了60FPS的動畫。

Ps. 其他卡頓原因
除了這兩個最主要的,還有一些個人遇到的小的細節也會有影響:

  • 動畫首幀響應不及時引起的動畫“不貼手”的感覺
  • 使用不合理的的動畫曲線/插值器

二、如何查看目前動畫的表現
覺得動畫比較卡這個事情實在太主觀了,我們需要有一點直觀的東西來支撐確認。

常見的手機屏幕的刷新率都是60Hz的,這樣就決定了我們的動畫的上限是60FPS,再高也意義不大了。
那麼每秒繪製60幀,每幀的時間就是 1000 / 60 = 16 ms

接下來看看這個
工具1: gfxinfo
這個可以用來查看最近繪製的每幀耗時,4.1以上的機器可以直接在【開發者選項 - GPU呈現模式分析】直接查看。4.1以下的就只能通過adb 去拉數據咯。

這裏寫圖片描述

通過這個直觀的查看,我們很快可以發現一些問題:
這條綠色的橫線表示16ms,柱狀圖高過綠線的說明這一幀繪製時間超過了標準。
柱狀圖分爲三部分,

藍色【Update DisplayList】
紅色【Process DisplayList】
黃色【Swap Buffers】

如果有某一條或者多條柱狀圖超過了標準很多,那麼這個動畫就可能存在比較大的性能問題(圖中的應用雖然有所超出,但是超出得不是很多,所以影響還不是很大)

另外一個當然就是在代碼中直接打點,計算動畫過程中View的onDraw的次數,再算出幀率,這個也是比較直觀的衡量。

三、針對不同的卡頓實施有針對性的方案

1. 啓用硬件加速

Android從3.0(API Level 11)開始,在繪製View的時候支持硬件加速,
充分利用GPU的特性,使得繪製更加平滑,但是會多消耗一些內存。

2. 降低平均單幀繪製時間,優化繪製方案

對於我們A1裏面提到的平均幀率不足的情況,多數是單幀的繪製時間過長,比如UI佈局太複雜,樹結構太深。
這裏推薦幾種優化的方式:

2.1 減少視圖樹層級
工具2: HierarchyViewer
該工具只能在開啓了ViewServer的機器上才能用,普通的商業手機會連接不上。只有像Google的官方機子,一些工程機(之前見過一臺小米的工程機)能被識別。通過某些途徑也可以給普通手機開啓ViewServer,不過自己試了失敗了。

該工具可以很直觀的看到 UI 的樹結構,哪個地方疊了太多無謂的層級基本上一目瞭然,你會發現的App總是在不經意間多嵌套了一個FrameLayout,可以使用TextView的LeftDrawable的時候想當然地用成了一個LinearLayout + ImageView + TextView 的組合。總之我們的目的就是讓視圖樹變得扁平,沒有太深的結構。

有一些文章提倡在複雜的場景下使用RelativeLayout去進行佈局,個人感覺弊端是會讓 UI 的代碼變得較難閱讀,需要不斷去查看佈局各個View之間的關係。所以建議只在真正必要優化的時候才進行,前期開發還是以易用爲重。

2.2 減少OverDraw(重複繪製)
趕緊先拿起你的App,打開 開發者模式-調試GPU過渡繪製, 再回到你的應用,好好看看哪些地方在渲染的時候會被重複繪製多次。
這裏寫圖片描述
一個好的App應該是不會有太多紅色的區域出現的。
//TO DO : 待填坑

2.3 避免在關鍵路徑上處理過多事情

這種事情經常出現在我們不經意寫出的代碼裏:

@Override
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    paint.setTextSize(15);
    canvas.drawText("重複創建paint對象", 0, 0, paint);
}

在這種Measure/Layout/Draw/getView等頻繁調用的路徑上需要儘量避免創建新的對象,進行 IO 處理等等耗時操作。頻繁的創建對象除了帶來頻繁執行的耗時,還會產生大量的內存碎片,在某一時刻觸發GC的時候又會引起不必要的麻煩。
儘管道理大家都明白,但是實際需求中總會有一些不經意的時候踩入這些坑。

e.g. 某ListView在getView的時候需要去取一些用來顯示的Data,getView經過層層請求,過了Controller,最後在Model處訪問了數據庫,於是列表的滑動就變得很坑爹了。
解決思路1:增加一個內存cache,緩存部分數據,減少IO操作。

2.3 有時候繪製Bitmap會比繪製一堆複雜的東西來得更快。
我們知道,一個Viiew到顯示需要經歷Measure-Layout-Draw的過程,那麼一個結構較爲複雜的ViewGroup在這個過程就會消耗較長的時間,導致繪製耗時較長;此外,一個自定義的View如果在Draw的過程中繪製了許多文字(文字的繪製要比基本圖形和貼圖耗時得多),也會有繪製的性能問題。
這個時候,如果能拿一張bitmap直接貼上去,減少了MeasureLayout或渲染文字的時間,那將大大提高效率(詳細參見後面的 預繪製

2.4 繪製Bitmap的時候可以通過犧牲顏色來換取速度 (以及減少內存)

在創建一個Bitmap的時候,可以選擇多種屬性:

Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);

RGB_565    //表示分別用5位,6位,5位來記錄紅,綠,藍的顏色值,沒有透明度
           //一個像素所佔內存 5+6+5 = 16bit = 2Bytes
ARGB_4444  //表示各用4位來記錄紅,綠,藍和透明度
ARGB_8888  //表示各用8位來記錄紅,綠,藍和透明度

可見 ARGB_8888的顯示效果最好,可以表現2^16種顏色,但是相應的弊端就是所佔的內存會比前兩者大一倍;更大的內存除了意味着內存爆表,對繪製的性能也會有很大影響。

故我們提倡,在使用大圖進行動畫時(截屏大小的),儘可能的在顯示效果妥協的情況下,使用RGB_565(無透明)和ARGB_4444(有透明)進行圖片的decode,對動畫幀率會有顯著的提升。

2.5 預先準備好動畫需要的內容
這裏分兩種
1. 預先加載資源 / 數據
2. 預繪製
//TO DO : 待填坑
預加載資源其實與按需加載的理念有一點點衝突。如果你的動畫過程需要某些圖片資源,數據等,那請記得在動畫開始之前就去做這些事情。比如我有一個窗口顯示,自帶一個滑入效果,顯示的內容有些數據要去做文件 IO, 故要麼在窗口動畫開始之前就做好數據的讀取(少量數據),要麼就在窗口滑入動畫完成後,顯示一個loadingView後再去讀取數據(大量數據)。前者就是一個預加載資源的例子~

而預繪製也是預加載的一種,只是它做得更徹底:我們預先準備的不僅是繪圖需要的數據,而是已經把數據全繪製好了,等到需要的時候把繪製好的緩存直接一貼就ok,大大減少繪製時間。
這裏也看一個具體的例子:

這裏寫圖片描述

如這個閱讀器,我們可以明顯看到上下兩層View,上層View是可以拖動的,下層是固定的。當我們翻下一頁時,下層的View露了出來,需要去獲取這一頁要顯示的文字,進行排版(行間距,兩端對齊等),然後進行繪製。由於我們是用手在拖動,直接與View進行交互,在我們往左拖動的時候,由於下層這個View的繪製需要150ms,就會發現手指往左移動了一小段距離後,整個下層View才能露出來,動畫就發生了延遲。用人話講就是這個動畫 不貼手

想想在 2.3 裏提到的,有時候繪製Bitmap會比繪製一堆複雜的東西來得更快,那我們完全可以這麼做:在還沒進行拖動的時候,我們先把下一頁的文字取出來,排好版,畫到另一個Bitmap上,當做一個Cache存起來。雖然這個過程我們依然需要150ms的繪製時間,但是這個時候畫面是靜止的!我們不是在動畫的時候去做這件事,不會有任何可見的影響~
而等到我們真正開始拖動的時候,我們只需要把這張緩存的bitmap貼到下層的View上,就可以顯示了!這個時間在測試機上相對於排版+繪製的150ms大大縮短,大概是40~60ms,節約了寶貴的幾幀。

其實這個方式影響的只是第一幀的繪製,當View被繪製出來後,在它動畫時,會有一個DrawingCache,也是一個Bitmap,加速動畫過程中View的繪製。所以這個方法優化的其實是動畫的 不貼手 而不是提高幀率

2.6 透明的貼圖,顏色等會更加消耗繪製性能
我們經常拿到設計師妹子給過來的UI標註是這樣的:

這裏寫圖片描述

也許設計師在設計的時候確實是用純黑再疊加一定的透明度來調整這個灰色的效果的,但是實際上這塊UI的展現並不需要透出底下的背景,那麼系統在渲染的過程中就白白浪費了一次透明混合的計算。在這種場景下,最好的方式自然是讓設計師直接給出準確的顏色值(使用#FFBFBFBF 而不是 #BF000000)

3. 找到讓你動畫掉幀的關鍵路徑!
在第 2 點中我們解決了整個動畫平均幀率不足的問題,但是還存在我們的動畫整體很流暢(FPS>40),卻在動畫過程中卡了一下的情況,我們需要尋找到在這一掉幀的瞬間發生了什麼。
工具3 :traceView
//TO DO : 挖坑 待寫
這裏用一個實際分析卡頓的例子來看看這個工具的簡單用法。。

e.g. Android 5.0 剛出時,在Nexus 5上發現閱讀器的某翻頁動畫非常卡,於是我們先開一下gfxinfo 看一下大概情況:

這裏寫圖片描述

呃。。。很明顯這些頂天的柱狀條便是造成我們卡頓的原因,這些單幀已經遠遠超出了綠色標註線16ms,所以我們下一步需要找到引起這些問題的具體代碼;

TraceView可以記錄在一段時間內,你的代碼裏各個函數塊的執行時間,也可以看到系統的一些關鍵方法的執行時間,可以將這些時間進行降序,找到最耗時的代碼塊。
使用方法可參照 這裏

這裏寫圖片描述

可以先關注這兩個屬性:
Incl Cpu Time:某函數佔用的CPU時間,包含內部調用其它函數的CPU時間
Excl Cpu Time:某函數佔用的CPU時間,但不含內部調用其它函數所佔用的CPU時間

上方紅箭頭所示的綠色色塊就代表下面紅框的drawPosText方法的耗時,可以看到大部分時間都花在了這個函數的執行上,導致到上面3000ms ~ 4000ms之間只有十幾幀~ 我們理想的效果是這樣的:單次耗時短,1s內畫得多

這裏寫圖片描述

好那麼我們就可以從下面drawPosText的Caller一層層向上找到我們自己的代碼,定位到問題。
這裏很幸運,往上追溯一層就是我自己的代碼了。原因確實是drawPosText的繪製耗時。

再往後就是針對這個特定的問題去查閱文檔,最後發現是AndroidL的preview版本在開啓硬件加速時這個 API 不支持,所以只要換掉這個 API 就ok了 (當然,現在的5.0版本已經修復了這個bug,是可以正常使用的了)。

從這裏例子我們可以簡單看到使用 TraceView 在性能分析的過程中起的作用,能夠很快定位到耗時操作,協助開發者解決問題。使用TraceView可以通過在Eclipse裏直接startTrace,也可以在代碼裏通過Debug.startMethodTracing()來打點;
前者方便易用,缺點是開啓了tracing的手機本來就比較卡了,會影響實際的判斷。後者好處是更準確,就是使用會相對麻煩一點。

p.s. 另外從這裏我們可以看到在 三.1 裏提到的開啓硬件加速帶來的弊端:某些API會在某些系統版本,某些特定Rom上失效,需要考慮各種兼容性問題。

4.其他一些零碎的優化點:

  • 避免和系統狀態欄,虛擬按鍵,輸入法一起做動畫;
    這個不太懂根本原因,但是儘可能避免這種場景。
    e.g. 應用在切換到全屏狀態同時要做一個動畫,比如彈一個菜單面板。如果這個動畫不要求一定馬上出來,那不妨稍等200ms,等頂部狀態欄的收起動畫做完再做菜單面板動畫,會流暢得多。
    e.g.2 輸入法頂部要掛一個擴展槽/類似QQ的輸入框;
    由於這個界面是隨着輸入法的彈出一起彈出的,那麼這個界面的淡入動畫就會比較卡。同上,我們稍等多150ms,等輸入法顯示完畢後再開始我們自己的動畫,效果就比較好了。

  • 減少動畫的主體對象和背景之間的視覺差異,顏色越接近,感覺越流暢;
    這是QQ瀏覽器的小說閱讀器,它的頁邊的陰影效果非常淡,由於這裏動畫的主體是陰影,那麼它與後面背景視覺差越小,陰影越透明,那麼就會顯得越流暢。
    這裏寫圖片描述

  • 不同動畫曲線的視覺效果
    在相同的幀率動畫下,不同的動畫曲線展現出來的視覺效果相差還是比較大的。在常見的位移動畫中,AccelerateDeccelerate 先加速後減速的效果通常狀況下會顯得更加自然。Animation/Animator的默認插值器都是Linear的,不妨試試換一個自然的曲線來體驗一下不同的效果。

    當然,你自己也可以寫一個Interpolator,實現它的getInterpolation方法便可自由定義自己的動畫插值器。
    另外,Android L 上已經給開發者們提供了一個非常有用的 PathInterpolator ,可以很方便的定義特殊的動畫曲線。

    再另外,推薦一個可以預覽不同動畫曲線的效果的小工具:動畫曲線工具
    你可以很方便地調節出一條曲線對應直觀的動畫效果是怎樣的,而如何把曲線實現在Interpolator上就需要研究一下貝塞爾曲線或者PathInterpolator咯。

  • 改變動畫的初始位置 / 初始值,來欺騙視覺
    這是一個比較tricky的方法,而且在某些時候會有掉幀的現象,個人不是很推薦,但是在特定場景下還是有用的。
    e.g. 做一個漸變的動畫,200ms內從無到有的顯示:
    由於我們對透明度非常低的時候的感知非常微弱,可以讓這個動畫不從0%開始漸變,而是30%開始,即我們在200ms內完成從30%到100%的效果,這樣動畫會更加平滑,缺點是閾值控制不好的話大家會發現那個從 0%alpha 到 30%alpha 的跳躍。

  • 幀率已經很難再提升了,換動畫方案:
    不同類型的動畫到達看起來差不多“同程度流暢”的幀率要求是不一樣的。同樣的時間內移動的越短自然越滑~
    所以位移動畫對幀率的要求最高,旋轉/縮放動畫次之,漸變動畫對幀率要求最低。
    如果你發現一個底部彈出菜單的效果優化到頭痛都比較卡,或者是在低端機上表現不好,那非常推薦把彈出動畫改成一個漸變的淡出動畫,儘管幀率可能差不多,但是視覺效果確實非常有效的。


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