iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 8

days[7] = "三顆渲染樹是如何運作的?(四)"

我保證這是渲染樹系列最後一篇了,我們將走訪完MyTimer的渲染和更新,看完你就會成為全台灣少數幾個真正瞭解三顆渲染樹運作方式的人了。還在看的人撐下去啊,終點就在眼前了!

回顧一下目前的渲染樹:
https://ithelp.ithome.com.tw/upload/images/20200907/201290536aIpOBht9F.png


我們上次進行到SingleChildRenderObjectElement的mount,而Element mount時主要會發生四件事

  1. 指定parent Element
  2. 如果自己是RenderObjectElement,呼叫自己管理的RenderObjectWidget來建立RenderObject,並幫RenderObject找到它的parent
  3. 如果是ComponentElement,呼叫自己管理的StatelessWidget/StatefulWidget進行build,產生新的child widget tree
  4. 呼叫updateChild,從自己的Widget的Child開始,繼續下一層的遞迴

我們現在進行到4.,這次的Widget是MyTimer,同樣經過updateChild->inflateWidget->createElement,建立了StatefulElement。值得注意的是StatefulElement在建立的同時,就在Constructor一併建立了State進行管理:

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget)

此時的渲染樹:
https://ithelp.ithome.com.tw/upload/images/20200908/20129053y914v73XoD.png


StatefulElement是ComponentElement,因此進行3.,呼叫MyTimer的build,產生Padding(child: RichText(...)):
https://ithelp.ithome.com.tw/upload/images/20200908/20129053jcl4JWmoZb.png


最後這兩個Widget都是RenderObjectWidget,流程都一樣我們就不重複了,直接快轉到建立完成:
https://ithelp.ithome.com.tw/upload/images/20200908/201290535jitwUADt1.png


好了終於要來更新Widget Tree了,我們來看看setState時發生了什麼事:

  // In State
  @protected
  void setState(VoidCallback fn) {
    ....
    final dynamic result = fn() as dynamic;
    ....
    _element.markNeedsBuild();
  }

  // In Element
  void markNeedsBuild() {
    ....
    _dirty = true;
    owner.scheduleBuildFor(this);
  }
  
  // In BuildOwner
  void scheduleBuildFor(Element element) {
    ....
    _dirtyElements.add(element);
    element._inDirtyList = true;
    ....
  }

這裡三層函數呼叫都有很長的例外處理,實際上的重點就是把Element設為dirty並加入dirty Element list,所有被加入的Element將在下一個Frame被更新。這裡渲染下一個Frame的呼叫根源來自Flutter Engine,再此先不細談,總之經過一連串呼叫後來到BuildOwner.buildScope:

  void buildScope(Element context, [ VoidCallback callback ]) {
      ....
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        ....
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          ....
        }
        index += 1
        ....
      }
      ....
  }

同樣很多例外處理,但基本上就是while loop呼叫每個dirtyElement的rebuild。接著經過rebuild->performRebuild->build來到我們的MyTimer的build函數。build完再次呼叫updateChild,有趣的事終於要發生了:

  // In Element
  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
      ....
      if (hasSameSuperclass && child.widget == newWidget) { // 1.
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {//2.
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget); // 3.
        ....
        newChild = child;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot); // 4.
      }
      ....
    return newChild;
  }

這次我們已經有child Element存在,也有新的Widget進來,因此我們要判斷

  1. 如果新的Widget和原本的一模一樣,直接回傳原本的Element
  2. 如果新的Widget和原本不一樣,再判斷Element能不能透過這個Widget來更新
  3. 如果可以,將child Element指向新的Widget。如果這是RenderObjectElement,同時進行RenderObject的更新
  4. 如果不行,使用新的Widget來建立新Element

這邊2.的判斷就是鼎鼎大名的canUpdate,如果你曾經看過任何一篇關於渲染樹的文章或影片,應該都會看到它。如果你還沒看過,現在就讓你看看。

  // In Widget
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

code本身就很簡單,再翻成白話就是,如果舊widget和新widget是同一類型,而且它們的key也是同一個(或都是null),就不需要重建Element,直接更新Element就好。
我們上次說updateChild是整個Widget系統的邏輯核心,而這個canUpdate正可以說是核心中的核心。如果看完這整個系列只記得一件事,請務必記得它。
以我們的範例來說舊和新widget都是Padding,也都沒有設key,因此canUpdate==true,而我們進入了3.,也就是child.update(newWidget)。
這時候child是SingleChildRenderObjectElement <- RenderObjectElement <- Element,因此update有這樣的call stack:

  // In Element
  void update(covariant Widget newWidget) {
    ....
    _widget = newWidget;
  }
  // In RenderObjectElement
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    ....
    widget.updateRenderObject(this, renderObject);
    ....
    _dirty = false;
  }
  // In SingleChildRenderObjectElement
  void update(SingleChildRenderObjectWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _child = updateChild(_child, widget.child, null);
  }

首先將自己的widget reference指向新widget,然後用新widget去更新renderObject,這裡Padding的updateRenderObject函數如下:

  void updateRenderObject(BuildContext context, RenderPadding renderObject) {
    renderObject
      ..padding = padding
      ..textDirection = Directionality.of(context);
  }

最後因為是SingleChildRenderObjectElement,再次呼叫updateChild進行下一層RichText的更新。後續的更新流程就跟Padding一模一樣了,這邊就不再覆述。


終於結束了!稍微整理一下吧,我們一開始講到Widget, Element, RenderObject三種class各自的職責和相互關係,接著介紹了這三種class的各種subclass和它們之間的對應關係,最後透過實際走訪範例APP從啟動、建立渲染樹到更新渲染樹的整個流程,進一步了解了三顆渲染樹的運作細節。當然,這些其實還不是全部,但我想對任何心智正常的人來說,瞭解到這邊已經非常足夠了。如果你能從第一篇看到最後一篇還能保持神智清醒,現在請給自己一個愛的鼓勵,台灣Flutter社群的未來就靠你了!


上一篇
days[6] = "三顆渲染樹是如何運作的?(三)"
下一篇
days[8] = "為什麼需要依賴注入?(上)"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

1 則留言

0
MarkFly~
iT邦新手 5 級 ‧ 2020-11-13 10:17:22

寫完這四篇真的太強強強強了/images/emoticon/emoticon12.gif

我要留言

立即登入留言