從原始碼瞭解Flutter的渲染基礎:Widget/Element/RenderObject

Michael周發表於2019-06-28

轉載請聯絡: 微訊號: michaelzhoujay

原文請訪問我的部落格


如果你要使用Flutter開始構建一個頁面,那麼你可能從Widget開始一層一層搭建。

但是你曾經想過這些Widget是如何繪製到螢幕上的嗎?很多同學應該會熟悉Flutter的三個樹形結構:

  • Widget Tree
  • Element Tree
  • RenderObject Tree

站在Flutter程式設計師的角度,打交道最多的應該是Widget了,那麼下面我帶領大家從Flutter原始碼讀起來看一下一個Widget是如何繪製到螢幕上的。

Opacity

首先我們來回憶一個熟悉的Widget:Opacity

這個Widget一般用來給一個佈局加上透明度,它只有一個引數 opacity,1.0表示完全不透明,0.0表示完全透明。

在原始碼裡可以看到 Opacity 的繼承關係:

Opacity -> SingleChildRenderObjectWidget -> RenderObject -> Widget

我們比較一下最簡單的 StatelessWidget/StatefulWidget :

StatelessWidget -> Widget

StatefulWidget -> Widget

可以看到Opacity多了SingleChildRenderObjectWidgetRenderObject這層關係,那這層關係到底是幹什麼的呢?我們接著往下看原始碼。

createRenderObject 方法

通過看RenderObject的程式碼,我們知道只要繼承了 RenderObject就需要實現createRenderObject方法。

@override
  RenderOpacity createRenderObject(BuildContext context) {
    return RenderOpacity(
      opacity: opacity,
      alwaysIncludeSemantics: alwaysIncludeSemantics,
    );
  }
複製程式碼

可以發現其實最終返回了一個RenderOpacity

RenderOpacity

大概看一下RenderOpacity的程式碼,可以知道這裡實現了最後的所謂的“繪製”也就是paint的程式碼:

void paint(PaintingContext context, Offset offset) {
    if (child != null) {
      if (_alpha == 0) {
        return;
      }
      if (_alpha == 255) {
        context.paintChild(child, offset);
        return;
      }
      assert(needsCompositing);
      context.pushOpacity(offset, _alpha, super.paint);
    }
  }
複製程式碼

這裡的PaintingContext實際上是個Canvas,所以最重要的程式碼就是context.pushOpacity這個方法的呼叫,最終用上了 _alpha,而這個_alpha由最上層OpacityWidget的唯一引數opacity計算而來。

所以我們來總結一下這裡面的關係:

  • Opacity直接繼承SingleChildRenderObjectWidget
  • Widget不直接參與繪製,僅僅儲存繪製需要的資訊
  • RenderOpacity 在這裡承擔了實際的 layout/render 邏輯

小結

我們日常所用的Widget僅僅是一些配置,RenderObject做了實際的 layout/render 等工作。這一點和Android的 View、iOS的 UIView 有本質的區別,請注意。

在Flutter中,只要是build()方法被呼叫了,就會建立一堆Widget,這個是OK的,上面說過Widget僅僅是配置檔案,所以即使頻繁地 建立/重建 它們,是不會帶來介面的重新整理的。

回想一下,如果你要在Flutter裡建立一個動畫,一般來說,需要在指定的Widget之上套上一層XXXTransition,然後動畫開始後用animationController來不斷地讓Widget重新整理重建,但是整個頁面不會因此重新重新整理。

所以,Widget的重建和頁面真正的重新渲染沒有直接的聯絡,那麼頁面什麼時候才會重新渲染呢,我們接著看原始碼。

Element

上面我們以Opacity為例,從原始碼中看出了它的繼承關係,並且推匯出一些結論,這裡面都沒有涉及到Element,但實際上Element也十分重要。那什麼是Element呢?原始碼裡的註釋如下:

An instantiation of a Widget at a particular location in the tree.

我們知道,Widget是immutable的,也就是說一旦有了變化,Widget是反覆重建的,你在程式碼裡寫的Widget的建構函式會被重新執行。那對應的,肯定得有一個東西來消化掉這些變化中的不變,來做cache。

讓程式設計師不去儲存Widgets是好的,因為他不會因為管理無數的Widget而且煩惱,一旦有變化就當整個頁面都在變化,Widget的每一幀對應了一個State。這樣,一個指定的state就能精確描述一個Widget該怎麼展示。也就是說,程式設計師只要管理好狀態,那麼就能管理好整個頁面變化的邏輯。

當一個Widget首次被建立的時候,那麼這個Widget會過Widget.createElement inflate成一個element,掛在 element tree 上。

此後,當State發生變化,則Widget重建,但Element只會updates。

也就是說我們能大概有一個以下的一個流程:

程式設計師寫Widget -> Widget形成Element -> Element構建 RenderObject -> RenderObject描述Canvas繪製 -> 交給FlutterEngine 光柵化Rasterize

回到我們今天的主角Opacity,從上面的分析,我們知道Opacity也是Widget,所以理應有對應的Element,但是上面看到為啥直接Widget就自己返回一個RenderOpacity呢?看起來是Widget自己建立了RenderObject。那麼它的element是什麼時候建立的呢?看原始碼:

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { 

...

@override
SingleChildRenderObjectElement createElement() => new SingleChildRenderObjectElement(this);

...

}
複製程式碼

Opacity繼承自SingleChildRenderObjectWidget, 這裡面的SingleChildRenderObjectElement,實際上是隻有一個child的Element。當第Widget第一次建立的時候,createElement會被呼叫,返回一個SingleChildRenderObjectElement

那它的 RenderObject 實際上是被這個 Element 建立的,看一下為什麼:

class SingleChildRenderObjectElement extends RenderObjectElement {

    SingleChildRenderObjectElement(SingleChildRenderObjectWidget widget) : super(widget);

    ...
}
複製程式碼

這個SingleChildRenderObjectElement建構函式接受了一個SingleChildRenderObjectWidget引數,它負責建立一個RenderObject。那麼我們看這個 renderobject 實際在哪裡建立的:

class SingleChildRenderObjectElement {
    ...

    @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _child = updateChild(_child, widget.child, null);
  }

  ...
}
複製程式碼

SingleChildRenderObjectElement裡有個mount方法,看起來沒啥,不過沒事兒我們往父類裡看:

class RenderObjectElement extends Element {

...

@override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    assert(() { _debugUpdateRenderObjectOwner(); return true; }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }

  ...
複製程式碼

終於看到magic了,所以在mount方法裡,element呼叫了widget.createRenderObject,並attach。

注意,這些都發生在mount方法裡,意味著只有element mounted的時候才會執行,所以只有一次。


參考文獻:

1. Flutter, what are Widgets, RenderObjects and Elements?

2. The Layer Cake

相關文章