[譯]理解 RenderThread

unkindleds發表於2018-03-12

原文連結:Understanding the RenderThread

RenderThread 是在 Android Lollipop 中新加入的元件。關於該元件的文件描述甚少,僅有一個模糊的定義:

RenderThread 是一個由系統管理的執行緒。較之 UI 執行緒的延遲,RenderThread 的動畫播放更為流暢。

為了解 RenderThread 的工作機制,需要介紹一些基本的概念。

當啟動硬體加速後,Android 使用 “display list” 元件進行繪製而非直接使用 CPU 繪製每一幀。Display List 是一系列繪製操作的記錄,抽象為 RenderNode 類。

這樣間接的進行繪製操作的優點頗多:

  1. Display List 可以按需多次繪製而無須同業務邏輯互動。
  2. 特定的繪製操作(如 translation, scale 等)可以作用於整個 display list 而無須重新分發繪製操作。
  3. 當知曉了所有繪製操作後,可以針對其進行優化:例如,所有的文字可以一起進行繪製一次。
  4. 可以將對 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 執行緒中執行。

那麼動畫是怎樣從不同的執行緒中進行更新的呢?

開啟硬體加速後,CanvasDisplayListCanvas 類實現,該類過載了部分繪製方法,方法引數型別使用 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 (通過 ViewAnimationUtilscreateCircularReveal 執行)

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);
}
複製程式碼

相關文章