深入淺出 RenderThread
RenderThread是Android Lollipop中引入的新組件,相關文檔很少。事實上,在我寫這篇文章的時候,只找到三篇相關引用,以及下面這個很模糊的定義:
RenderThread是一個新的由系統控制的處理線程,它可以在UI線程阻塞時保持動畫平滑。
為了理解其真實功能,我們需要先介紹幾個概念。
當設備開啟硬件加速時,Android不再在每一幀內都執行繪制任務,而是使用一個叫做“展示列表”的(隱藏的)組件,它通過RenderNode類(曾經是DisplayList類)記錄繪制操作集合。
這種間接的方式可以帶來諸多好處:
-
一個展示列表可以被多次繪制,而不需要重新執行業務邏輯。
-
特定的操作(如轉換、放縮等等)可以覆蓋整個列表,無需重新安排某個繪制操作。
-
一旦所有的繪制操作已知,就可以進行優化:比如,如果可能,所有的文字都一起繪制。
-
展示列表的處理工作可能可以分發給另一個線程執行。
上面的第四點正是RenderThread的工作之一:處理優化操作與GPU分發,減輕UI線程的壓力。
在Lollipop之前你可能會注意到,進行如Activity切換等重量級工作時,想要使View屬性動畫平滑進行是不可能的。而在Lollipop以后,這些動畫,包括如水波等其他效果在相同場景下竟可以流暢進行,其中就依靠RenderThread的幫助。
渲染的真正執行者是GPU,而它自己是不懂任何動畫的。展示動畫的唯一方法就是對于每一幀發布不同的繪制指令,這個邏輯不是GPU可以處理的。當這些邏輯需要在UI線程中執行時,重量級工作會妨礙新的繪制指令及時發布,于是產生卡頓現象,無論在進行哪種動畫。
前面提到了, RenderThread可以負責展示列表流水線的部分工作,但要注意展示列表的創建與修改還是需要在UI線程中完成。
那么如何在子線程中更新動畫呢?
當通過硬件加速進行繪制時,Canvas的實現類叫做DisplayListCanvas(曾經叫GLES20Canvas),它有許多繪制方法的重載方法,這些方法不是接收一個直接的參數,而是一個CanvasProperty的引用,這個CanvasProperty封裝了需要的參數值。這樣一來在UI線程創建的展示列表仍然可以靜態地調用繪制方法,而且這些調用的參數可以通過CanvasProperty映射被動態修改(在RenderThread中異步修改)。
之后還有一步:CanvasProperty的值需要通過RenderNodeAnimator來隨時間變動,由此動畫被配置并啟動。
產生的動畫有這些有趣的屬性:
-
目標DisplayListCanvas:需要被人工設定,且之后不可以再修改。
-
即發即棄:一旦被啟動就只能被取消,也就是說無法暫停/繼續。而且不能知道當下的值。
-
可以提供一個自定義的Interpolator,其代碼會在RenderThread中調用。
-
如有延遲啟動,會在RenderThread中進行等待。
下面是 能在RenderThread中操作的動畫(到現在為止):
View屬性(可以通過View.animate訪問):
-
變換(X、Y、Z)
-
放縮(X、Y)
-
旋轉(X、Y)
-
透明度Alpha
-
圓形展開動畫 Circular Reveal
Canvas方法(通過Canvas屬性)
-
畫圈drawCircle(centerX, centerY, radius, paint)
-
畫圓角矩形drawRoundRect(left, top, right, bottom, cornerRadiusX, cornerRadiusY, paint)
Paint屬性
-
透明度Alpha
-
寬度Stroke Width
看起來Google只是封裝了所有實現Material Design動畫所需的繪制操作。這看起來雖然很有限,但是只需要一點創造力就可以實現各種不同的動畫,從各種水波動畫到全新的效果。這樣的動畫操作的好處是可以提供不在UI線程運行的不卡頓的動畫。
現在看起來在Android N中RenderThread的能力會被加強(比如AnimatedVectorDrawable將會在其中完成),或許有朝一日它會進入public API。
我可以讓我自己的動畫運行在RenderThread中嗎?
官方的簡短回答:除了View.animate與ViewAnimationUtils.createCircularReveal提供的動畫都不可以。
非官方的長一些的回答:本文所說的每一個組件都是隱藏的,所以如果要使用哪個組件都需要通過反射獲得所需類和方法的引用,進行封裝以保證類型安全,提供獲取失敗的回調方法等等。詳情見我的 這個repo
或許這種方法不應該被實際應用,這一點需要你自己把握。
使用RenderThread很簡單,一般有三步:
CanvasProperty<Float> centerXProperty;
CanvasProperty<Float> centerYProperty;
CanvasProperty<Float> radiusProperty;
CanvasProperty<Paint> paintProperty;
Animator radiusAnimator;
Animator alphaAnimator;
@Override
protected void onDraw(Canvas canvas) {
if (!animationInitialised) {
// 1. 創建繪制動畫所需要的所有CanvasProperty
centerXProperty = RenderThread.createCanvasProperty(canvas, initialCenterX);
centerYProperty = RenderThread.createCanvasProperty(canvas, initialCenterY);
radiusProperty = RenderThread.createCanvasProperty(canvas, initialRadius);
paintProperty = RenderThread.createCanvasProperty(canvas, paint);
// 2. 創建一個或多個Animator,與你想操作的屬性對應
radiusAnimator = RenderThread.createFloatAnimator(this, canvas, radiusProperty, targetRadius);
alphaAnimator = RenderThread.createPaintAlphaAnimator(this, canvas, paintProperty, targetAlpha);
radiusAnimator.start();
alphaAnimator.start();
}
// 3. 繪制到Canvas上
RenderThread.drawCircle(canvas, centerXProperty, centerYProperty, radiusProperty, paintProperty);
}
在上面的Repo中有完整的示例。
歡迎關注我的公眾號“androidway”,將零碎時間都用在刷干貨上!
來自: http://blog.chengdazhi.com/index.php/190