# HenCoder Android 自定義 View 1-8 硬體加速

扔物線發表於2017-09-18

硬體加速這個詞每當被提及,很多人都會感興趣。這個詞給大部分人的概念大致有兩個:快速、不穩定。對很多人來說,硬體加速似乎是一個只可遠觀而不可褻玩的高階科技:是,我聽說它很牛逼,但我不敢「亂」用,因為我怕 hold 不住。

今天我試著就把硬體加速的外衣脫掉(並沒有),聊一聊它的原理和應用:

  1. 硬體加速的本質和原理;
  2. 硬體加速在 Android 中的應用;
  3. 硬體加速在 Android 中的限制。

本篇是 「HenCoder Android 開發進階」自定義 View 部分的最後一篇:硬體加速。

如果你沒聽說過 HenCoder,也可以看看這個:
HenCoder:給高階 Android 工程師的進階手冊

概念

在正式開始之前需要說明一下,作為繪製部分的最後一期,本期內容只是為了內容的完整性做一個補充,因為之前好幾期的內容裡都有涉及硬體加速的技術點,而一些讀者因為不瞭解硬體加速而產生了一些疑問。所以僅僅從難度上來講,這期的內容並不難,並且本期的大部分內容你都可以從這兩個頁面中找到:

  1. Hardware Acceleration | Android Developers
  2. Google I/O 2011: Accelerated Android Rendering

下面進入正題。

所謂硬體加速,指的是把某些計算工作交給專門的硬體來做,而不是和普通的計算工作一樣交給 CPU 來處理。這樣不僅減輕了 CPU 的壓力,而且由於有了「專人」的處理,這份計算工作的速度也被加快了。這就是「硬體加速」。

而對於 Android 來說,硬體加速有它專屬的意思:在 Android 裡,硬體加速專指把 View 中繪製的計算工作交給 GPU 來處理。進一步地再明確一下,這個「繪製的計算工作」指的就是把繪製方法中的那些 Canvas.drawXXX() 變成實際的畫素這件事。

原理

在硬體加速關閉的時候,Canvas 繪製的工作方式是:把要繪製的內容寫進一個 Bitmap,然後在之後的渲染過程中,這個 Bitmap 的畫素內容被直接用於渲染到螢幕。這種繪製方式的主要計算工作在於把繪製操作轉換為畫素的過程(例如由一句 Canvas.drawCircle() 來獲得一個具體的圓的畫素資訊),這個過程的計算是由 CPU 來完成的。大致就像這樣:

而在硬體加速開啟時,Canvas 的工作方式改變了:它只是把繪製的內容轉換為 GPU 的操作儲存了下來,然後就把它交給 GPU,最終由 GPU 來完成實際的顯示工作。大致是這樣:

如圖,在硬體加速開啟時,CPU 做的事只是把繪製工作轉換成 GPU 的操作,這個工作量相對來說是非常小的。

怎麼就「加速」了?

從上面的圖中可以看出,硬體加速開啟後,繪製的計算工作由 CPU 轉交給了 GPU。不過這怎麼就能起到「加速」作用,讓繪製變快了呢?

硬體加速能夠讓繪製變快,主要有三個原因:

  1. 本來由 CPU 自己來做的事,分攤給了 GPU 一部分,自然可以提高效率;
  2. 相對於 CPU 來說,GPU 自身的設計本來就對於很多常見型別內容的計算(例如簡單的圓形、簡單的方形)具有優勢;
  3. 由於繪製流程的不同,硬體加速在介面內容發生重繪的時候繪製流程可以得到優化,避免了一些重複操作,從而大幅提升繪製效率。

其中前兩點可以總結為一句:用了 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 的引入而提高了繪製效率,還由於繪製機制的改變,而極大地提高了介面內容改變時的重新整理效率。

所以把上面的三條壓縮總結一下,硬體加速更快的原因有兩條:

  1. 用了 GPU,繪製變快了;
  2. 繪製機制的改變,導致介面內容改變時的重新整理效率極大提高。

限制

如果僅僅是這樣,硬體加速只有好處沒有缺陷,那大家都不必關心硬體加速了,這篇文章也不會出現:既然是好東西就用唄,關心那麼多原理幹嗎?

可事實就是,硬體加速不只是好處,也有它的限制:受到 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 來開啟硬體加速這麼兩個引數,而是三個引數,在 SOFTWAREHARDWARE 之外還有一個 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 的:

  1. 引數為 LAYER_TYPE_SOFTWARE 時,使用軟體來繪製 View Layer,繪製到一個 Bitmap,並順便關閉硬體加速;
  2. 引數為 LAYER_TYPE_HARDWARE 時,使用 GPU 來繪製 View Layer,繪製到一個 OpenGL texture(如果硬體加速關閉,那麼行為和 VIEW_TYPE_SOFTWARE 一致);
  3. 引數為 LAYER_TYPE_NONE 時,關閉 View Layer。

View Layer 可以加速無 invalidate() 時的重新整理效率,但對於需要呼叫 invalidate() 的重新整理無法加速。

View Layer 繪製所消耗的實際時間是比不使用 View Layer 時要高的,所以要慎重使用。

相關文章