原文連結:Understanding the RenderThread
RenderThread 是在 Android Lollipop 中新加入的元件。關於該元件的文件描述甚少,僅有一個模糊的定義:
RenderThread 是一個由系統管理的執行緒。較之 UI 執行緒的延遲,RenderThread 的動畫播放更為流暢。
為了解 RenderThread 的工作機制,需要介紹一些基本的概念。
當啟動硬體加速後,Android 使用 “display list” 元件進行繪製而非直接使用 CPU 繪製每一幀。Display List 是一系列繪製操作的記錄,抽象為 RenderNode
類。
這樣間接的進行繪製操作的優點頗多:
- Display List 可以按需多次繪製而無須同業務邏輯互動。
- 特定的繪製操作(如 translation, scale 等)可以作用於整個 display list 而無須重新分發繪製操作。
- 當知曉了所有繪製操作後,可以針對其進行優化:例如,所有的文字可以一起進行繪製一次。
- 可以將對 display list 的處理轉移至另一個執行緒(非 UI 執行緒)。
最後一點恰好是 RenderThread 負責的:在 UI 執行緒外執行優化操作與將繪製操作分發給 GPU。
在 Lollipop 之前,執行“重”操作的同時對 View 屬性執行動畫(例如在 Activity 間執行 transition 動畫)幾乎不可能。而 Lollipop 及以上的 Android 版本,這類動畫以及其它一些效果(例如 ripple )卻可以流暢的執行。這樣的黑科技便源自 RenderThread 的幫助。
渲染工作的真正執行者是 GPU,而 GPU 對於動畫一無所知:執行動畫的唯一途徑便是將每一幀的不同繪製操作分發給 GPU,但該邏輯本身不能在 GPU 上執行。而如果在 UI 執行緒執行該操作,任意的重操作都將阻塞新的繪製指令及時分發,於是動畫便出現了延遲。
如前文所述,RenderThread 可以處理 display list 流程的某些部分,但要注意到 display list 的建立和修改仍需要在 UI 執行緒中執行。
那麼動畫是怎樣從不同的執行緒中進行更新的呢?
開啟硬體加速後,Canvas
由 DisplayListCanvas
類實現,該類過載了部分繪製方法,方法引數型別使用 CanvasProperty
物件替換原有的基本型別(例如用 CanvasProperty<Float>
替換 float
型別),CanvasProperty
是原有型別的包裝類。這樣,dislplay list 及其對應的繪製操作便可以在 UI 執行緒中建立,而繪製方法的引數可以通過 CanvasProperty
的對映動態地、通過 RenderThread 非同步地修改。
實現上述操作還需一步:CanvasProperty
的值需要通過 RenderNodeAnimator
執行動畫操作,RenderNodeAnimator
中包含了動畫的配置及初始值。
此類動畫有一些有趣的特性:
- 目標
DisplayListCanvas
需要手動設定且不可修改 - 該動畫傳送後無須關注:動畫開始執行後只能取消(沒有暫停和恢復)且無法知曉此刻的屬性值
- 可以設定自定義插值器,插值器將在 RenderThread 執行
- start delay 一般在 RenderThread 上等待
時至今日,可以通過 RenderThread 執行的動畫如下:
View 屬性 (通過 View 的 animate
方法執行)
- Translation (X, Y, Z)
- Scale (X, Y)
- Rotation (X, Y)
- Alpha
- Circular reveal animation (通過
ViewAnimationUtils
的createCircularReveal
執行)
Canvas 方法與 Canvas 屬性
- drawCircle(centerX, centerY, radius, paint)
- drawRoundRect(left, top, right, bottom, cornerRadiusX, cornerRadiusY, paint)
Paint 屬性
- Alpha
- Stroke width
似乎 Google 僅將 Material Design 動畫中所需的繪製操作進行了封裝。
在 Android N 及以上,RenderThread 的能力的得以擴充(例如,AnimatedVectorDrawable
將使用 RenderThread 執行動畫),也許今後 RenderThread 會成為開放 API。
簡而言之,我可以在 RenderThread 執行動畫嗎?
依據文件,不可以。 除非使用 View.animate
或是 ViewAnimationUtils.createCircularReveal
。
通過 Hack ,可以實現。 對於未開放的元件,我們都可以通過反射獲取對相關類、方法的引用,通過包裝類以保證型別安全,反射失敗時提供反饋等等。
筆者實現了使用 RenderThread 執行動畫的庫。
使用該庫十分簡單,僅需 3 步:
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. create as many CanvasProperty as needed with the initial animation values
centerXProperty = RenderThread.createCanvasProperty(canvas, initialCenterX);
centerYProperty = RenderThread.createCanvasProperty(canvas, initialCenterY);
radiusProperty = RenderThread.createCanvasProperty(canvas, initialRadius);
paintProperty = RenderThread.createCanvasProperty(canvas, paint);
// 2. create one or more Animator with the properties you want to animate
radiusAnimator = RenderThread.createFloatAnimator(this, canvas, radiusProperty, targetRadius);
alphaAnimator = RenderThread.createPaintAlphaAnimator(this, canvas, paintProperty, targetAlpha);
radiusAnimator.start();
alphaAnimator.start();
}
// 3. draw to the Canvas
RenderThread.drawCircle(canvas, centerXProperty, centerYProperty, radiusProperty, paintProperty);
}
複製程式碼