破譯Android性能優化中的16ms問題

當你不能向六歲的兒童講清楚一件事的時候,說明你還沒有真正理解這件事。

Android應用有一個明顯的趨勢---越來越多地使用動畫效果來提升用戶體驗。但任何事情都是有代價的,豐富復雜的動畫提升用戶體驗的同時,性能問題像隱形的惡魔一樣,逐漸地侵蝕著你的應用。動畫不流暢、界面卡頓開始困擾著你,逼著你進行性能優化。在這個優化過程中,最理想的標準就是繪制一幀的時間不要超過16ms。這是什么意思?讓我們一探究竟。

一、屏幕刷新頻率

我們知道,手機屏幕是由許多的像素點組成的,如下圖所示:

lcd_pixels.jpg

通過讓每一個像素點顯示不同的顏色,可以組合成各種各樣的圖像。這些像素點的顏色數據從哪里來?

答案是:在GPU控制的一塊緩沖區中,這塊緩沖區叫做Frame Buffer(也就是幀緩沖區)。你可以把它簡單理解成一個二維數組,數組中的每一個元素對應著手機屏幕上的一個像素點,元素的值代表著屏幕上對應的像素點要顯示的顏色。

Frame Buffer中的數據是不斷變化的,為了應對這種變化,手機屏幕的邏輯電路會定期用Frame Buffer中的數據刷新屏幕上的像素點。目前,主流的刷新頻率是60次/秒,折算出來就是16ms刷新一次。

二、Frame Buffer中的數據是怎么來的?

GPU除了Frame Buffer,用以交給手機屏幕進行繪制外,還有一個緩沖區,叫Back Buffer,這個Back Buffer 用以交給你的應用,讓你往里面填充數據。GPU會定期交換Back Buffer和Frame Buffer,也就是讓Back Buffer 變成Frame Buffer交給屏幕進行繪制,讓原先的Frame Buffer變成Back Buffer交給你的應用進行繪制。交換的頻率也是60次/秒,這就與屏幕硬件電路的刷新頻率保持了同步。如下圖所示:

switch-buffer.png

三、丟幀是怎么發生的?

上面說GPU會定期交換Back Buffer和Frame Buffer,但有一個例外情況,當你的應用 正在 往Back Buffer中填充數據時,系統會將Back Buffer鎖定。如果到了GPU交換兩個Buffer的時間點,你的應用還在往Back Buffer中填充數據,GPU會發現Back Buffer被鎖定了,它會放棄這次交換,后果就是手機屏幕仍然顯示原先的圖像。

最不幸的情況是,GPU剛剛放棄這次交換,你的應用就完成了對Back Buffer的數據填充。可憐的你必須等待下一個16ms時間,才能看到這次數據填充的效果。

在這種情況下,從Back Buffer鎖定開始,也就是你的應用開始往Back Buffer中填充數據,到填充后的數據展示到屏幕上,需要的時間是32ms。

我們知道,所謂的應用往Back Buffer中填充數據,其實就是更新你的應用的Activity的界面。我們假設更新前后的界面是這樣的:

ball-bump.png

很簡單,也就是讓紅色的小球向上移動了一段距離。但由于你的應用沒能在16ms內完成界面更新,導致你的用戶盯著第一個屏幕看了32ms,然后發現小球“ ”到了一個新的高度,而不是平滑地移動到了新的高度。

上面所說的情況稱作“丟幀”。

四、作為開發者,怎樣優化應用避免丟幀?

作為應用開發者,為了讓用戶有流暢的動畫體驗,我們優化的目標就是不要丟幀,也就是在動畫進行的過程中,我們要確保更新一幀的時間不要超過16ms。那么,怎樣做才能盡可能接近這個目標呢?有如下幾個tips:

  1. 減少視圖層次,盡量使用扁平化的視圖布局,如使用RelativeLayout代替多層嵌套的LinearLayout。
  2. 減少不必要的View的invalidate調用。
  3. 去除View中不必要的background,因為許多background并不會顯示在最終的屏幕上。比如ImageView, 假如它顯示的圖片填滿了它的空間,你就沒有必要給它設置一個背景色。

以上是三個操作性很強的建議。好奇的你可能會問,這樣做的理由是什么?

前面說過,系統將Back Buffer 交給你的應用填充數據,實際過程是將Back Buffer鎖定后,將一個指向它的引用交給你的應用,這個引用就是一個Canvas對象。你的應用獲取這個Canvas對象后,會按照視圖層次從上往下遍歷傳給每一個View,View在onDraw方法中接收到的canvas對象就是它,如下:

proteced void onDraw(Canvas canvas)

View用這個canvas對象完成自己的繪制。每個View都完成自己的繪制后,才算完成了一幀的繪制。

減少視圖層次,可以減少傳遞canvas對象時間。

同時,Android提供的所有控件以及你自定義的控件,在onDraw方法中都會調用 super.onDraw方法,而在這個方法中會執行繪制background的操作,如果這個background最終不會顯示,繪制它顯然是在浪費時間。

關于第二點,減少不必要的invalidate調用,一方面是為了減少重繪,同時,也是為了配合GPU,最大限度地利用好緩存,這里涉及到GPU的工作細節,不展開了。

明白了原理,該怎么做你心里就會有數,比如在onDraw方法中,減少創建對象,尤其是復雜的對象等,都是為了縮短繪制的時間。

最后,你還應當明白,這16ms不是全給你繪制界面的,還有layout、measure呢,Android的一些子系統也要占用這寶貴的16ms完成一些自己的任務,真正留給你繪制自己的界面的時間肯定是少于16ms,你能做的就是盡可能減少自己的繪制時間。

好了,這篇文章中,我沒有涉及GPU工作的細節,目的是在屏蔽底層技術實現的同時讓每一個層次的Android開發者都能從整體上理解把握所謂的16ms。

 

來自:http://www.jianshu.com/p/a769a6028e51

 

 本文由用戶 veay7775 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!