硬體加速這個詞每當被提及,很多人都會感興趣。這個詞給大部分人的概念大致有兩個:快速、不穩定。對很多人來說,硬體加速似乎是一個只可遠觀而不可褻玩的高階科技:是,我聽說它很牛逼,但我不敢「亂」用,因為我怕 hold 不住。
今天我試著就把硬體加速的外衣脫掉(並沒有),聊一聊它的原理和應用:
- 硬體加速的本質和原理;
- 硬體加速在 Android 中的應用;
- 硬體加速在 Android 中的限制。
本篇是 「HenCoder Android 開發進階」自定義 View 部分的最後一篇:硬體加速。
如果你沒聽說過 HenCoder,也可以看看這個:
HenCoder:給高階 Android 工程師的進階手冊
概念
在正式開始之前需要說明一下,作為繪製部分的最後一期,本期內容只是為了內容的完整性做一個補充,因為之前好幾期的內容裡都有涉及硬體加速的技術點,而一些讀者因為不瞭解硬體加速而產生了一些疑問。所以僅僅從難度上來講,這期的內容並不難,並且本期的大部分內容你都可以從這兩個頁面中找到:
下面進入正題。
所謂硬體加速,指的是把某些計算工作交給專門的硬體來做,而不是和普通的計算工作一樣交給 CPU 來處理。這樣不僅減輕了 CPU 的壓力,而且由於有了「專人」的處理,這份計算工作的速度也被加快了。這就是「硬體加速」。
而對於 Android 來說,硬體加速有它專屬的意思:在 Android 裡,硬體加速專指把 View 中繪製的計算工作交給 GPU 來處理。進一步地再明確一下,這個「繪製的計算工作」指的就是把繪製方法中的那些 Canvas.drawXXX()
變成實際的畫素這件事。
原理
在硬體加速關閉的時候,Canvas
繪製的工作方式是:把要繪製的內容寫進一個 Bitmap
,然後在之後的渲染過程中,這個 Bitmap
的畫素內容被直接用於渲染到螢幕。這種繪製方式的主要計算工作在於把繪製操作轉換為畫素的過程(例如由一句 Canvas.drawCircle()
來獲得一個具體的圓的畫素資訊),這個過程的計算是由 CPU 來完成的。大致就像這樣:
而在硬體加速開啟時,Canvas
的工作方式改變了:它只是把繪製的內容轉換為 GPU 的操作儲存了下來,然後就把它交給 GPU,最終由 GPU 來完成實際的顯示工作。大致是這樣:
如圖,在硬體加速開啟時,CPU 做的事只是把繪製工作轉換成 GPU 的操作,這個工作量相對來說是非常小的。
怎麼就「加速」了?
從上面的圖中可以看出,硬體加速開啟後,繪製的計算工作由 CPU 轉交給了 GPU。不過這怎麼就能起到「加速」作用,讓繪製變快了呢?
硬體加速能夠讓繪製變快,主要有三個原因:
- 本來由 CPU 自己來做的事,分攤給了 GPU 一部分,自然可以提高效率;
- 相對於 CPU 來說,GPU 自身的設計本來就對於很多常見型別內容的計算(例如簡單的圓形、簡單的方形)具有優勢;
- 由於繪製流程的不同,硬體加速在介面內容發生重繪的時候繪製流程可以得到優化,避免了一些重複操作,從而大幅提升繪製效率。
其中前兩點可以總結為一句:用了 GPU,繪製就是快。原因很直觀,不再多說。
關於第三點,它的原理我大致說一下:
前面說到,在硬體加速關閉時,繪製內容會被 CPU 轉換成實際的畫素,然後直接渲染到螢幕。具體來說,這個「實際的畫素」,它是由 Bitmap
來承載的。在介面中的某個 View 由於內容發生改變而呼叫 invalidate()
方法時,如果沒有開啟硬體加速,那麼為了正確計算 Bitmap
的畫素,這個 View
的父 View、父 View 的父 View 乃至一直向上直到最頂級 View,以及所有和它相交的兄弟 View
,都需要被呼叫 invalidate()
來重繪。一個 View 的改變使得大半個介面甚至整個介面都重繪一遍,這個工作量是非常大的。
而在硬體加速開啟時,前面說過,繪製的內容會被轉換成 GPU 的操作儲存下來(承載的形式稱為 display list,對應的類也叫做 DisplayList
),再轉交給 GPU。由於所有的繪製內容都沒有變成最終的畫素,所以它們之間是相互獨立的,那麼在介面內容發生改變的時候,只要把發生了改變的 View 呼叫 invalidate()
方法以更新它所對應的 GPU 操作就好,至於它的父 View 和兄弟 View,只需要保持原樣。那麼這個工作量就很小了。
正是由於上面的原因,硬體加速不僅是由於 GPU 的引入而提高了繪製效率,還由於繪製機制的改變,而極大地提高了介面內容改變時的重新整理效率。
所以把上面的三條壓縮總結一下,硬體加速更快的原因有兩條:
- 用了 GPU,繪製變快了;
- 繪製機制的改變,導致介面內容改變時的重新整理效率極大提高。
限制
如果僅僅是這樣,硬體加速只有好處沒有缺陷,那大家都不必關心硬體加速了,這篇文章也不會出現:既然是好東西就用唄,關心那麼多原理幹嗎?
可事實就是,硬體加速不只是好處,也有它的限制:受到 GPU 繪製方式的限制,Canvas
的有些方法在硬體加速開啟式會失效或無法正常工作。比如,在硬體加速開啟時, clipPath()
在 API 18 及以上的系統中才有效。具體的 API 限制和 API 版本的關係如下圖:
所以,如果你的自定義控制元件中有自定義繪製的內容,最好參照一下這份表格,確保你的繪製操作可以正確地在所有使用者的手機裡能夠正常顯示,而不是隻在你的執行了最新版本 Android 系統的 Nexus 或 Pixel 裡測試一遍沒問題就釋出了。小心被祭天。
不過有一點可以放心的是,所有的原生自帶控制元件,都沒有用到 API 版本不相容的繪製操作,可以放心使用。所以你只要檢查你寫的自定義繪製就好。
View Layer
在之前幾期的內容裡我提到過幾次,如果你的繪製操作不支援硬體加速,你需要手動關閉硬體加速來繪製介面,關閉的方式是通過這行程式碼:
view.setLayerType(LAYER_TYPE_SOFTWARE, null);複製程式碼
有不少人都有過疑問:什麼是 layer type?如果這個方法是硬體加速的開關,那麼它的引數為什麼不是一個 LAYER_TYPE_SOFTWARE
來關閉硬體加速以及一個 LAYER_TYPE_HARDWARE
來開啟硬體加速這麼兩個引數,而是三個引數,在 SOFTWARE
和 HARDWARE
之外還有一個 LAYER_TYPE_NONE
?難道還能既不用軟體繪製,也不用硬體繪製嗎?
事實上,這個方法的本來作用並不是用來開關硬體加速的,只是當它的引數為 LAYER_TYPE_SOFTWARE
的時候,可以「順便」把硬體加速關掉而已;並且除了這個方法之外,Android 並沒有提供專門的 View 級別的硬體加速開關,所以它就「順便」成了一個開關硬體加速的方法。
setLayerType()
這個方法,它的作用其實就是名字裡的意思:設定 View Layer 的型別。所謂 View Layer,又稱為離屏緩衝(Off-screen Buffer),它的作用是單獨啟用一塊地方來繪製這個 View ,而不是使用軟體繪製的 Bitmap 或者通過硬體加速的 GPU。這塊「地方」可能是一塊單獨的 Bitmap
,也可能是一塊 OpenGL 的紋理(texture,OpenGL 的紋理可以簡單理解為影像的意思),具體取決於硬體加速是否開啟。採用什麼來繪製 View 不是關鍵,關鍵在於當設定了 View Layer 的時候,它的繪製會被快取下來,而且快取的是最終的繪製結果,而不是像硬體加速那樣只是把 GPU 的操作儲存下來再交給 GPU 去計算。通過這樣更進一步的快取方式,View 的重繪效率進一步提高了:只要繪製的內容沒有變,那麼不論是 CPU 繪製還是 GPU 繪製,它們都不用重新計算,而只要只用之前快取的繪製結果就可以了。
多說一句,其實這個離屏緩衝(Off-screen Buffer),更準確的說應該叫做離屏快取(Off-screen Cache)會更合適一點。原因在上面這一段裡已經說過了,因為它其實是快取而不是緩衝。(這段話僅代表個人意見)
基於這樣的原理,在進行移動、旋轉等(無需呼叫 invalidate()
)的屬性動畫的時候開啟 Hardware Layer 將會極大地提升動畫的效率,因為在動畫過程中 View 本身並沒有發生改變,只是它的位置或角度改變了,而這種改變是可以由 GPU 通過簡單計算就完成的,並不需要重繪整個 View。所以在這種動畫的過程中開啟 Hardware Layer,可以讓本來就依靠硬體加速而變流暢了的動畫變得更加流暢。實現方式大概是這樣:
view.setLayerType(LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(LAYER_TYPE_NONE, null);
}
});
animator.start();複製程式碼
或者如果是使用 ViewPropertyAnimator
,那麼更簡單:
view.animate()
.rotationY(90)
.withLayer(); // withLayer() 可以自動完成上面這段程式碼的複雜操作複製程式碼
不過一定要注意,只有你在對 translationX
translationY
rotation
alpha
等無需呼叫 invalidate()
的屬性做動畫的時候,這種方法才適用,因為這種方法本身利用的就是當介面不發生時,快取未更新所帶來的時間的節省。所以簡單地說——
這種方式不適用於基於自定義屬性繪製的動畫。一定記得這句話。
另外,除了用於關閉硬體加速和輔助屬性動畫這兩項功能外,Layer 還可以用於給 View 增加一些繪製效果,例如設定一個 ColorMatrixColorFilter
來讓 View 變成黑白的:
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
view.setLayerType(LAYER_TYPE_HARDWARE, paint);複製程式碼
另外,由於設定了 View Layer 後,View 在初次繪製時以及每次 invalidate()
後重繪時,需要進行兩次的繪製工作(一次繪製到 Layer,一次從 Layer 繪製到螢幕),所以其實它的每次繪製的效率是被降低了的。所以一定要慎重使用 View Layer,在需要用到它的時候再去使用。
總結
本期內容就到這裡,就像開頭處我說的,本期只是作為一個完整性的補充,並沒有太多重要或高難度的東西,我也沒有準備視訊或太多的截圖或動圖來做說明。慣例總結一下:
硬體加速指的是使用 GPU 來完成繪製的計算工作,代替 CPU。它從工作分攤和繪製機制優化這兩個角度提升了繪製的速度。
硬體加速可以使用 setLayerType()
來關閉硬體加速,但這個方法其實是用來設定 View Layer 的:
- 引數為
LAYER_TYPE_SOFTWARE
時,使用軟體來繪製 View Layer,繪製到一個 Bitmap,並順便關閉硬體加速; - 引數為
LAYER_TYPE_HARDWARE
時,使用 GPU 來繪製 View Layer,繪製到一個 OpenGL texture(如果硬體加速關閉,那麼行為和VIEW_TYPE_SOFTWARE
一致); - 引數為
LAYER_TYPE_NONE
時,關閉 View Layer。
View Layer 可以加速無 invalidate()
時的重新整理效率,但對於需要呼叫 invalidate()
的重新整理無法加速。
View Layer 繪製所消耗的實際時間是比不使用 View Layer 時要高的,所以要慎重使用。