Flutter - 從樹開始了解Flutter

本文不是一篇介紹Flutter入門知識,主要包括Flutter的視圖管理和渲染機(jī)制。如果在閱讀中有任何問題,麻煩Q442953298。歡迎喜歡Flutter的同學(xué)交流

Flutter中的樹

Widget樹

對Flutter有一定了解的同學(xué),可能知道Flutter存在Everything’s a Widget的說法。在我們構(gòu)建一個FlutterApp的時候,從main函數(shù)開始就調(diào)起了runApp函數(shù)。而runApp傳入的參數(shù)就是一個Widget。

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page')),
    );
  }
}

MyApp -> MaterialApp -> MyHomePage -> ...
組成了一個Widget樹。
一個Widget的簡單結(jié)構(gòu)如下

// DiagnosticableTree 是一個調(diào)試類
// Wiget可以說是表現(xiàn)層的基類
abstract class Widget extends DiagnosticableTree {
    final Key key;
    @protected
    Element createElement();
}

而在StatelessWidget 和 State類中都實現(xiàn)了

Widget build(BuildContext context) {}

通過build方法父Widget可以訪問到子節(jié)點(diǎn)的Widget

幾個問題

  1. Stateless的Build方法由它本身實現(xiàn),StatefulWidget的Build方法由State類實現(xiàn)。這種設(shè)計是出于對性能和數(shù)據(jù)-狀態(tài)的響應(yīng)等多方面的考慮。
    • 并不是所有的部件都需要關(guān)心數(shù)據(jù)/狀態(tài)的變化,比如布局類、文本、圖片等。他們對于狀態(tài)的變化應(yīng)該是無感知的。所以他們直接調(diào)用本身的Build方法來就可以輕松構(gòu)造表現(xiàn)層。
    • StatefulWidget是有狀態(tài)的Widget,這種說法其實是存在問題的。StatefulWidget繼承自Widget,而Widget被@immutable標(biāo)記,所以StatefulWidget本身也是不可變的。他之所以能夠響應(yīng)狀態(tài)的變化,完全依賴于State類的實現(xiàn)。
    • 在Wiget樹更新的時候,必然伴隨著部件的創(chuàng)建和銷毀.如果將狀態(tài)交付給其他類管理,則可以在響應(yīng)狀態(tài)時輕松的改變Widget樹而不影響數(shù)據(jù)層的維護(hù)。這樣設(shè)計是數(shù)據(jù)層和表現(xiàn)層分離的良好體現(xiàn)。
  2. Widget類由@immutable標(biāo)記,狀態(tài)不可變。
    剛才我們說Widget的可變狀態(tài)都是由State類進(jìn)行管理,因此Widget自身的狀態(tài)其實都是靜態(tài)的,Widget從構(gòu)造到被釋放,自身的狀態(tài)都應(yīng)該保持不變。

Element樹

Element - An instantiation of a [Widget] at a particular location in the tree.
一個在樹中特定位置的Widget實例。
我們討論Widget的時候提到Widget實現(xiàn)了一個方法:

@protected
  Element createElement();

在StatelessWidget和StatefulWidget重寫了這個方法

abstract class StatelessWidget extends Widget {
    StatelessElement createElement() => StatelessElement(this);
}
abstract class StatefulWidget extends Widget {
  StatefulElement createElement() => StatefulElement(this);
}

StatelessElement構(gòu)造方法只是簡單的將Widget傳給父類構(gòu)造方法

StatelessElement(StatelessWidget widget) : super(widget);

StatefulElement的構(gòu)造方法則不太相同

StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;
    _state._widget = widget;
  }
// 1. 調(diào)用傳入的widget createState 創(chuàng)建了State對象
// 2. 賦值給自己的_state變量(可用get方法訪問)
// 3. state對象分別持有了自身 和 傳入的widget對象。

StatelessElement 和StatefulElement都繼承自ComponentElement。ComponentElement聲明了一個抽象方法

Widget build();

StatelessElement和StatefulElement都重寫了build方法

// stateless實現(xiàn)
  @override
  Widget build() => widget.build(this);
// stateful實現(xiàn)
  @override
  Widget build() => state.build(this);

根據(jù)上面的實現(xiàn)我們可以簡單的得出一個結(jié)論:

Widget的build都是由Element類的build方法觸發(fā)。

Widget樹的構(gòu)建就和Element樹就有了密不可分的關(guān)系,他們到底是怎么實現(xiàn)的。

runApp()以后發(fā)生了什么

  1. runApp(Widget app)

    void runApp(Widget app) {
      WidgetsFlutterBinding.ensureInitialized()
        ..attachRootWidget(app)
        ..scheduleWarmUpFrame();
    // WidgetsFlutterBinding.ensureInitialized() 創(chuàng)建了一個WidgetsFlutterBinding對象,他的主要任務(wù)就是協(xié)調(diào)Framework層和Application層的交互、進(jìn)程、渲染等底層任務(wù)
    }
    
  2. attachRootWidget(app)

    // WidgetsFlutterBinding.dart 
     void attachRootWidget(Widget rootWidget) {
        _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
          container: renderView,
          debugShortDescription: '[root]',
          child: rootWidget
        ).attachToRenderTree(buildOwner, renderViewElement);
      }
    // 首先RenderObjectToWidgetAdapter 創(chuàng)建了 RenderObjectToWidgetAdapter對象
    // 接著attachToRenderTree 則生成了renderViewElement 
    // renderViewElement 通過RenderObjectToWidgetAdapter的createElement創(chuàng)建,它繼承自RenderObjectToWidgetElement
    
    RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
    
    // RenderObjectToWidgetElement的widget字段持有了RenderObjectToWidgetAdapter對象
    
  3. WidgetsFlutterBinding 持有了Element樹的根節(jié)點(diǎn)RenderObjectToWidgetElement;
    RenderObjectToWidgetElement持有了Widget樹的根節(jié)點(diǎn) RenderObjectToWidgetAdapter

  1. 關(guān)于RenderObjectToWidgetAdapter

    RenderObjectToWidgetAdapter({
    this.child,
    this.container,
    this.debugShortDescription
    }) : super(key: GlobalObjectKey(container));
    // 在第一第二塊的attachRootWidget方法里我們將app當(dāng)做child參數(shù)傳給了RenderObjectToWidgetAdapter
    RenderObjectToWidgetAdapter<RenderBox>(
          container: renderView,
          debugShortDescription: '[root]',
          child: rootWidget
        )
    final Widget child;
    
    // 其中還有個參數(shù)需要被注意:
    container的參數(shù)傳入的變量是renderView。
    renderView 在WidgetsFlutterBinding的構(gòu)造方法里被創(chuàng)建,他S是后續(xù)我們會講到的RenderObject樹的根節(jié)點(diǎn)
    我們可以通過 WidgetsFlutterBinding.renderView或者WidgetsFlutterBinding.pipelineOwner.rootNode訪問到它
    
  2. 關(guān)于RenderObjectToWidgetElement

    RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
        if (element == null) {
          owner.lockState(() {
            element = createElement();
            element.assignOwner(owner);
          });
          owner.buildScope(element, () {
            element.mount(null, null);
          });
        } else {
          element._newWidget = this;
          element.markNeedsBuild();
        }
        return element;
    }
    // 在attachToRenderTree方法里會判斷element參數(shù)是否為null如果為null則調(diào)用CreateElement,否則更新已存在的element的widget
    
  3. element.mount(null, null);
    上面我們已經(jīng)提到

    WidgetsFlutterBinding -> element根節(jié)點(diǎn) RenderObjectToWidgetElement -> widget根節(jié)點(diǎn)RenderObjectToWidgetAdapter

    element.mount 方法則開啟了Element樹和Widget樹構(gòu)建的大門

    // RenderObjectToWidgetElement mount 掛載
    void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _rebuild();
    }
    // 我們先不關(guān)心 super.mount在RenderObjectToWidgetElement的父類做了什么
    // _rebuild方法
    void _rebuild() {
        try {
          _child = updateChild(_child, widget.child, _rootChildSlot);
          // _child 作為參數(shù)傳入第一次為null
          // widget.child 
          // widget是RenderObjectToWidgetAdapter
          // RenderObjectToWidgetAdapter.child 則是我們最初runApp傳入的appWidget
        } catch (exception, stack) {
           // 拋出我們在頁面上看見的紅色背景黃字警告頁面
        }
    }
    

    我們終于看到我們最初傳入的AppWidget有了用武之地。

  4. updateChild方法 是構(gòu)建和更新Widget樹和Element樹的靈魂

    Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        if (newWidget == null) {
          if (child != null)
            deactivateChild(child);
          return null;
        }
        if (child != null) {
          if (child.widget == newWidget) {
            // 此處為更新slot代碼
            return child;
          }
          if (Widget.canUpdate(child.widget, newWidget)) {
            // 此處為更新slot代碼
            child.update(newWidget);
            return child;
          }
          deactivateChild(child);
          }
        return inflateWidget(newWidget, newSlot);
    }
    

    讓我們用通俗的語言來描述這段代碼的邏輯
    1.如果傳入的newWidget為空而child不為空,則表明這是一次更新樹的操作。newWidget為空表示我們在移除了一個Widget節(jié)點(diǎn)和它子Widget樹,因此需要我們移除Element對應(yīng)的child節(jié)點(diǎn)

    2.如果child不為空(表明是更新操作):

    2.1如果widget和新傳入的newWidget是同一個對象,則說明這個節(jié)點(diǎn)沒有任何變化返回child即可。(此處表明該節(jié)點(diǎn)可能是InheritWidget或者GlobalKey做了標(biāo)記,此處后面的文章分析)

    2.2.如果widget與新傳入的newWidget不是同一塊內(nèi)存,但是Widget.canUpdate(new,old) 的 runtimeType以及key都相同,則表明這個widget的深度和類型都沒有發(fā)生改變,此時child這個Element對象可以繼續(xù)拿來復(fù)用,只需要將舊的widget更新為新的即可。(此處在更新操作里最常見,比如setState后,build方法沒有改變child節(jié)點(diǎn)的地方,則該節(jié)點(diǎn)下的所有widget都會走此處)

    2.3如果上面的情況都不滿足,比如widget的節(jié)點(diǎn)雖然沒有變化但是key發(fā)生了變化,再比如widget節(jié)點(diǎn)的類型由Text轉(zhuǎn)換為Image類型,則這個節(jié)點(diǎn)的Element不能再復(fù)用,此時需要我們先移除Element節(jié)點(diǎn)

    3.當(dāng)child為空,或者widget的key或類型發(fā)生了變化,就會走到這里,此時會調(diào)用inflateWidget方法,將新widget膨脹為一個Element。

    此處用膨脹其實是想說明Element的作用更加底層和重要

  5. inflateWidget魔法-一種遞歸創(chuàng)建Widget樹和Element的方式

    Element inflateWidget(Widget newWidget, dynamic newSlot) {
        assert(newWidget != null);
        final Key key = newWidget.key;
        if (key is GlobalKey) {
          final Element newChild = _retakeInactiveElement(key, newWidget);
          if (newChild != null) {
            return updatedChild;
          }
        }
        final Element newChild = newWidget.createElement();
        newChild.mount(this, newSlot);
        return newChild;
    }
    

    Globalkey此處的邏輯先不表,后續(xù)系列再主要介紹,當(dāng)我們重新create了一個newChild的Element后,Element調(diào)用了自身的mount方法。
    此處有了一個大的設(shè)想,StatelessElement和StatefulElement的mout方法將會把他們build方法獲得的widget繼續(xù)updateChild-inflateWidget方式傳遞下去因此去做了驗證

  6. ComponentElement
    StatelessElement 和 StatefulElement都繼承自ComponentElement。在ComponentElement的mount方法里存在

    void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _firstBuild();
    }
    // _firstBuild()
    void _firstBuild() {
        rebuild();
    }
    // rebuild在element中實現(xiàn),最后會調(diào)用performRebuild
    //performRebuild 在ComponentElement中實現(xiàn)
    void performRebuild() {
        Widget built;
        try {
          built = build();
         
        } catch (e, stack) {
          //error 處理
        } finally {
          _dirty = false;
        }
        try {
          _child = updateChild(_child, built, slot);
          assert(_child != null);
        } catch (e, stack) {
          //error 處理
        }
    }
    

    最終在performRebuild方法里知道了我們需要的東西。element會調(diào)用自身的build實現(xiàn),獲取當(dāng)前widget節(jié)點(diǎn)下的build子節(jié)點(diǎn)。這個build方法創(chuàng)建的子節(jié)點(diǎn)會在調(diào)用updateChild方法的時候傳入,因此形成了遞歸,一層一層的去構(gòu)建或者說更新我們的element樹,和Widget樹。也就完整的完成了Element樹和Widget樹的調(diào)用。

SetState以后發(fā)生了什么

我們說剛才的流程是一個樹創(chuàng)建的流程,那么樹的更新是怎么開始的了?

  1. State. setState

    _element.markNeedsBuild();

  2. Element. didChangeDependencies

    markNeedsBuild();
    這兩者都會引發(fā)markNeedsBuild()

void markNeedsBuild() {
    _dirty = true;
    owner.scheduleBuildFor(this);
}
// markNeedsBuild則會標(biāo)記element被污染

scheduleBuildFor方法會將element add -> owner._dirtyElements里
并且標(biāo)記element的_inDirtyList 為 true
然后發(fā)起一個等待Vsync信號渲染事件,當(dāng)下一個Vsync信號來臨時,會調(diào)用DrawFrame方法
此時buildScope方法重新被調(diào)用
你可能沒有記住這個方法,你可以去上面查找一下attachWidgetTree 沒錯在第五節(jié)。
在attachToRenderTree方法里,如果element是新建的也會調(diào)用這個方法,這個方法的目的就是清空owner的_dirtyElements的element。
清空的方式就是調(diào)用他們的rebuild方法

_dirtyElements[index].rebuild();

因此回到了上面我們說的流程,更新和構(gòu)建的區(qū)別,只在于構(gòu)建是從頂點(diǎn)開始,并且是element樹與Widget樹向下交叉構(gòu)建,而更新的流程會在Widget節(jié)點(diǎn)沒有類型和key變化的前提下優(yōu)先保留element,新建Widget節(jié)點(diǎn)的子樹。更新的方式也是向下交叉的遍歷。

RenderObject樹

經(jīng)過上面的介紹,可能存在很多復(fù)雜的邏輯繞來繞去,如果沒有耐心查看源代碼,大可以簡單的認(rèn)為,element樹和Widget樹的掛載是向下交叉完成的。
但我們了解Element樹的意義似乎并沒有表現(xiàn)出來,因為Element沒有渲染合成相關(guān)的代碼。所以可以認(rèn)為Element并不是Layer合成的幫助者,這時候需要引入一個之前介紹樹構(gòu)建邏輯時忽略的對象RenderObject。

RenderObject的主要實現(xiàn)我們后續(xù)系列再表,它的主要作用就是將Widget的布局屬性進(jìn)行l(wèi)ayout paint CompositingBits 等方式的計算,最后將渲染的計算結(jié)果提交圖形引擎。
在Element的實現(xiàn)中

  RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child
      if (element is RenderObjectElement)
        result = element.renderObject;
      else
        element.visitChildren(visit);
    }
    visit(this);
    return result;
  }

如果此對象是RenderObjectElement,則渲染對象是樹中此位置處的對象。否則,這個getter將沿著樹走下去,直到找到一個RenderObjectElement。
根節(jié)點(diǎn)的element 其實繼承自RenderObjectElement
RnderObjectElement持有了一個_renderObeject對象,這個對象是由element.widget createRenderObject創(chuàng)建出來的。
在根節(jié)點(diǎn)RenderObjectToWidgetAdpater中返回的是RenderObjectWithChildMixin的類,這個類是在RenderObject的Mixin類,他實際返回的是WidgetFlutterBinding中的renderView 也即是pipelineOwner的rootNode。

不是每個Widget都存在CreateRenderObject方法。CreateRenderObject被聲明在Widget的子類RenderObjectWidget中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容