Flutter渲染流程解析

一. Flutter的渲染流程

1.1. Widget - Element - RenderObject關系

1.2. Widget是什么?

官方對Widget的說明:

  • Flutter的Widgets的靈感來自React,中心思想是構造你的UI使用這些Widgets。
  • Widget使用配置和狀態(tài),描述這個View(界面)應該長什么樣子。
  • 當一個Widget發(fā)生改變時,Widget就會重新build它的描述,框架會和之前的描述進行對比,來決定使用最小的改變(minimal changes)在渲染樹中,從一個狀態(tài)到另一個狀態(tài)。

自己的理解:

  • Widget就是一個個描述文件,這些描述文件在我們進行狀態(tài)改變時會不斷的build。
  • 但是對于渲染對象來說,只會使用最小的開銷來更新渲染界面。

1.3. RenderObject

官方對RenderObject的描述:

  • 渲染樹上的一個對象。
  • RenderObject層是渲染庫的核心。

Flutter引擎渲染的時候,其實渲染的是RenderObjectTree,但是widget和RenderObject并不是一一對應的,為什么呢?因為有些widget其實就是一個盒子,將其他widget裝到一起的。

就比如我們常用的 Text 繼承于 StatelessWidget,我們知道,只要繼承于StatelessWidget或者繼承于StatefulWidget,就要看它的build方法,看過build方法之后我們發(fā)現它最后返回一個RichText,這個RichText繼承于MultiChildRenderObjectWidget這才是最終渲染的widget。

1.4. Element是什么?

官方對Element的描述:

  • Element是一個Widget的實例,在樹中詳細的位置。
  • Widget描述和配置子樹的樣子,而Element實際去配置在Element樹中特定的位置。

Element其實就相當于React中的虛擬DOM,我們先來理解一下前端里面的虛擬DOM。

當我們書寫js生成的HTML代碼,這時候會直接操作真實的DOM,操作真實DOM是非常消耗性能的,所以React和Vue都有虛擬DOM的概念,什么意思呢?就是當我們通過js操作HTML,我們會先去操作虛擬DOM,虛擬DOM中通過diff算法,判斷哪些DOM需要修改,甚至不需要修改,最后把虛擬DOM打個補丁到真實DOM上,這樣做的好處就是我們可以以最小的開銷來更新真實的DOM。

我們再看一下上面的三棵樹,Widget就相當于HTML代碼,Element就相當于虛擬DOM,Render就相當于真實DOM。

當我們創(chuàng)建一個Widget的時候,我們也許就不需要創(chuàng)建一個新的Render對象,我們先去看看保存的Element的類型和key是否一致,如果一致,就直接修改屬性即可,這樣我們就沒必要創(chuàng)建新的Render Object,也許只是修改其中某個屬性就行,這樣就做到了以最小的開銷來更新Render Object。

二. 源碼查看對象的創(chuàng)建過程

我們先給widget做個分類:

這些是組件Widget,不會生成RenderObject
Container()
Text()
HYHomeContent()

這些是渲染Widget,會生成RenderObject
Padding()
Row()

我們這里以Padding為例,Padding是用來設置內邊距,我們看看這個Widget最后怎么生成RenderObject的。

2.1. Widget

Padding是一個Widget,并且繼承自SingleChildRenderObjectWidget

繼承關系如下:

Padding  -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget

Container繼承關系如下:

Container -> StatelessWidget -> Widget

我們之前在創(chuàng)建Widget時,經常使用StatelessWidget和StatefulWidget,這種Widget只是將其他的Widget在build方法中組裝起來,并不是一個真正可以渲染的Widget(在之前的課程中其實有提到)。

在Padding的類中,我們找不到任何和渲染相關的代碼,這是因為Padding僅僅作為一個配置信息,這個配置信息會隨著我們設置的屬性不同,頻繁的銷毀和創(chuàng)建。

問題:頻繁的銷毀和創(chuàng)建會不會影響Flutter的性能呢?

那么真正的渲染相關的代碼在哪里執(zhí)行呢?

  • RenderObjectWidget

2.2. RenderObjectWidget

我們來看Padding里面的代碼,有一個非常重要的方法:

  • 這個方法其實是來自RenderObjectWidget的類,在這個類中它是一個抽象方法;
  • 抽象方法是必須被子類實現的,但是它的子類SingleChildRenderObjectWidget也是一個抽象類,所以可以不實現父類的抽象方法;
  • 但是Padding不是一個抽象類,必須在這里實現對應的抽象方法,而它的實現就是下面的實現;
@override
RenderPadding createRenderObject(BuildContext context) {
  return RenderPadding(
    padding: padding,
    textDirection: Directionality.of(context),
  );
}

上面的代碼創(chuàng)建了什么呢?RenderPadding

RenderPadding的繼承關系是什么呢?

RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject

我們來具體查看一下RenderPadding的源代碼:

  • 如果傳入的_padding和原來保存的value一樣,那么直接return;
  • 如果不一致,調用_markNeedResolution,而_markNeedResolution內部調用了markNeedsLayout;
  • 而markNeedsLayout的目的就是標記在下一幀繪制時,需要重新布局performLayout;
  • 如果我們找的是Opacity,那么RenderOpacity是調用markNeedsPaint,RenderOpacity中是有一個paint方法的;
  set padding(EdgeInsetsGeometry value) {
    assert(value != null);
    assert(value.isNonNegative);
    if (_padding == value)
      return;
    _padding = value;
    _markNeedResolution();
  }

2.3. Element

我們來思考一個問題:

  • 之前我們寫的大量的Widget在樹結構中存在引用關系,但是Widget會被不斷的銷毀和重建,那么意味著這棵樹非常不穩(wěn)定;
  • 那么由誰來維系整個Flutter應用程序的樹形結構的穩(wěn)定呢?
  • 答案就是Element。
  • 官方的描述:Element是一個Widget的實例,在樹中詳細的位置。

我們再研究Padding是怎么創(chuàng)建Element的,我們進入Widget類里面,發(fā)現有個createElement()方法:

@protected
@factory
Element createElement();

因為Widget是個抽象類,所以createElement方法必須被它的子類實現。我們也可以得出一個結論,只要你是一個widget,無論是不是渲染的widget,都要實現createElement方法,只不過每個類實現的不一樣。

我們發(fā)現,對于Padding,是父類SingleChildRenderObjectWidget實現了這個方法,最后返回的是SingleChildRenderObjectElement。

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

對于Container,也是它的父類StatelessWidget實現了createElement方法:

@override
StatelessElement createElement() => StatelessElement(this);

同理,StatefulWidget也實現了createElement方法:

@override
StatefulElement createElement() => StatefulElement(this);

它們返回的對象不同,一個是StatelessElement,一個是StatefulElement,只不過都繼承于ComponentElement。它們的區(qū)別就是StatefulElement會多一個state屬性。

小總結

  1. 我們寫一個widget
  2. 對于渲染widget會創(chuàng)建RenderObject
  3. 每一個widget都會創(chuàng)建一個Element對象
  4. 在創(chuàng)建完一個Element之后,Flutter引擎會調用mount方法來將Element插入到樹中具體的位置

Element什么時候創(chuàng)建?

在每一次創(chuàng)建Widget的時候,會創(chuàng)建一個對應的Element,然后將該元素插入樹中。

在SingleChildRenderObjectWidget中,我們可以找到如下代碼:

  • 在Widget中,Element被創(chuàng)建,并且在創(chuàng)建時,將this(Widget)傳入了,Element就保存了對Widget的應用;
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);

在創(chuàng)建完一個Element之后,Flutter引擎會調用mount方法來將Element插入到樹中具體的位置,再Element類中我們會找到如下代碼:

進入ComponentElement源碼,查看ComponentElement的mount的執(zhí)行過程,代碼比較繁瑣,可以直接看下面總結。

abstract class ComponentElement extends Element {
  /// Creates an element that uses the given widget as its configuration.
  ComponentElement(super.widget);

  Element? _child;

  bool _debugDoingBuild = false;
  @override
  bool get debugDoingBuild => _debugDoingBuild;

  @override
  // 1. 調用mount方法
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_lifecycleState == _ElementLifecycle.active);
    // 2. 調用_firstBuild
    _firstBuild();
    assert(_child != null);
  }

  void _firstBuild() {
    // StatefulElement overrides this to also call state.didChangeDependencies.
    // 3. 調用rebuild
    rebuild(); // This eventually calls performRebuild.
  }

  /// Calls the [StatelessWidget.build] method of the [StatelessWidget] object
  /// (for stateless widgets) or the [State.build] method of the [State] object
  /// (for stateful widgets) and then updates the widget tree.
  ///
  /// Called automatically during [mount] to generate the first build, and by
  /// [rebuild] when the element needs updating.
  @override
  @pragma('vm:notify-debugger-on-exception')
  // 6. 這是performRebuild
  void performRebuild() {
    assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
    // 8.  就是這個Widget
    Widget? built;
    try {
      assert(() {
        _debugDoingBuild = true;
        return true;
      }());
      // 7. 調用build方法生成一個Widget
      built = build();
      assert(() {
        _debugDoingBuild = false;
        return true;
      }());
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      _debugDoingBuild = false;
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () => <DiagnosticsNode>[
            if (kDebugMode)
              DiagnosticsDebugCreator(DebugCreator(this)),
          ],
        ),
      );
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () => <DiagnosticsNode>[
            if (kDebugMode)
              DiagnosticsDebugCreator(DebugCreator(this)),
          ],
        ),
      );
      _child = updateChild(null, built, slot);
    }
  }

  /// Subclasses should override this function to actually call the appropriate
  /// `build` function (e.g., [StatelessWidget.build] or [State.build]) for
  /// their widget.
  @protected
  Widget build();

  @override
  void visitChildren(ElementVisitor visitor) {
    if (_child != null) {
      visitor(_child!);
    }
  }

  @override
  void forgetChild(Element child) {
    assert(child == _child);
    _child = null;
    super.forgetChild(child);
  }
}

// 4. 這是rebuild
void rebuild() {
  assert(_lifecycleState != _ElementLifecycle.initial);
  if (_lifecycleState != _ElementLifecycle.active || !_dirty) {
    return;
  }
  Element? debugPreviousBuildTarget;
  performRebuild();
}

/// Cause the widget to update itself.
///
/// Called by [rebuild] after the appropriate checks have been made.
@protected
// 5. 調用performRebuild
void performRebuild();
}

class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget super.widget);

  @override
  // 9. 拿到widget,調用widget的build方法
  // 這個widget就是創(chuàng)建element的時候傳進來的widget
  Widget build() => (widget as StatelessWidget).build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

上面1-9步,看起來比較復雜,其實就是:

mount方法 -> firstBuild -> rebuild -> performBuild -> build -> _widget的build

這里的_widget就是創(chuàng)建element的時候傳進來的widget。

我們都知道build方法有個參數build(Build Context context),所以這個context其實就是element,這個context最主要的作用就是告訴我們構建的element在樹里面的哪個位置,之后可以沿著樹去查找一些信息。

如果是statefulWidget,它里面的build方法如下:

@override
Widget build() => state.build(this);

我們發(fā)現,它之后調用了state.build(this),而不是 (widget as StatelessWidget).build(this);

下面我們看看SingleChildRenderObjectElement的mount方法的調用過程。

在調用mount方法時,會同時使用Widget來創(chuàng)建RenderObject,并且保持對RenderObject的引用,創(chuàng)建完RenderObject之后再把RenderObject掛載到RenderObjectTree樹的某個位置

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    // 就是這行代碼,創(chuàng)建RenderObject
    _renderObject = widget.createRenderObject(this);
    assert(() {
      _debugUpdateRenderObjectOwner();
      return true;
    }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }

下面說一下StatefulElement,它是繼承于ComponentElement的,所以ComponentElement有的方法,它都有。

  StatefulElement(StatefulWidget widget)
      // 1. 就是這里,調用了createState
      : _state = widget.createState(),
        super(widget) {
    assert(() {
      if (!state._debugTypesAreRight(widget)) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
          ErrorDescription(
            'The createState function for ${widget.runtimeType} returned a state '
            'of type ${state.runtimeType}, which is not a subtype of '
            'State<${widget.runtimeType}>, violating the contract for createState.',
          ),
        ]);
      }
      return true;
    }());
    assert(state._element == null);
    state._element = this;
    assert(
      state._widget == null,
      'The createState function for $widget returned an old or invalid state '
      'instance: ${state._widget}, which is not null, violating the contract '
      'for createState.',
    );
    // 2. 然后將widget賦值給state里面的_widget
    state._widget = widget;
    assert(state._debugLifecycleState == _StateLifecycle.created);
  }

上面主要做了兩件事

  1. StatefulElement的構造器中調用了widget.createState()方法
  2. 將widget賦值給state里面的_widget,正是因為這樣,我們在state里面才可以通過this.widget拿到對應的widget

總結:

  1. widget創(chuàng)建完之后,Flutter框架一定會根據widget創(chuàng)建一個element,創(chuàng)建完之后會調用element的mount方法,最后根據一系列的調用會調用widget的build(Build Context context)方法。
  2. 如果是renderElement,那么它的mount主要做的就是創(chuàng)建一個_renderObject
  3. 如果是StatefulElement,那么會調用調用了createState,然后將widget賦值給state里面的_widget

2.4. build的context是什么

在StatelessElement中,我們發(fā)現是將this傳入,所以本質上BuildContext就是當前的Element。

Widget build() => widget.build(this);

我們來看一下繼承關系圖:

  • Element是實現了BuildContext類(隱式接口)
abstract class Element extends DiagnosticableTree implements BuildContext

在StatefulElement中,build方法也是類似,調用state的build方式時,傳入的是this。

Widget build() => state.build(this);

2.5. 創(chuàng)建過程小結

Widget只是描述了配置信息:

  • 其中包含createElement方法用于創(chuàng)建Element;
  • 也包含createRenderObject,但是不是自己在調用;

Element是真正保存樹結構的對象:

  • 創(chuàng)建出來后會由framework調用mount方法;
  • 在mount方法中會調用widget的createRenderObject對象;
  • 并且Element對widget和RenderObject都有引用;

RenderObject是真正渲染的對象:

  • 其中有markNeedsLayout performLayout markNeedsPaint paint等方法;

三. Widget的key

在我們創(chuàng)建Widget的時候,總是會看到一個key的參數,它又是做什么的呢?

3.1. key的案例需求

我們一起來做一個key的案例需求。

home界面的基本代碼:

class _HYHomePageState extends State<HYHomePage> {
  List<String> names = ["aaa", "bbb", "ccc"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Test Key"),
      ),
      body: ListView(
        children: names.map((name) {
          return ListItemLess(name);
        }).toList(),
      ),

      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: () {
          setState(() {
            names.removeAt(0);
          });
        }
      ),
    );
  }
}

注意:待會兒我們會修改返回的ListItem為ListItemLess或者ListItemFul。

3.2. StatelessWidget的實現

我們先對ListItem使用一個StatelessWidget進行實現:

class ListItemLess extends StatelessWidget {
  final String name;
  final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  ListItemLess(this.name);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 60,
      child: Text(name),
      color: randomColor,
    );
  }
}

它的實現效果是每刪除一個,所有的顏色都會發(fā)現一次變化。

  • 原因非常簡單,刪除之后調用setState,會重新build,重新build出來的新的StatelessWidget會重新生成一個新的隨機顏色。

3.3. StatefulWidget的實現(沒有key)

我們對ListItem使用StatefulWidget來實現:

class ListItemFul extends StatefulWidget {
  final String name;
  ListItemFul(this.name): super();
  @override
  _ListItemFulState createState() => _ListItemFulState();
}

class _ListItemFulState extends State<ListItemFul> {
  final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 60,
      child: Text(widget.name),
      color: randomColor,
    );
  }
}

我們發(fā)現一個很奇怪的現象,顏色不變化,但是數據向上移動了。

首先,我們知道,三個widget對應了三個StatefulElement,StatefulElement里面又有state指向對應的widget,然后我們調用setState的時候,widget樹肯定全部都會重建,但是element不會重建,因為它會調用widget里面的canUpdate方法:

  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

當從三個widget -> 兩個widget的時候,會比較新舊widget的runtimeType和key,可以發(fā)現,最后肯定會返回true,所以三個state的前面兩個不會重新創(chuàng)建,最后一個state沒有對應的widget就會直接刪掉(調用unmount),這時候Element Tree里面就保留了前兩個原來的element,然后將前兩個element掛載到那兩個新的widget上了,所以我們才看到刪除最后一個的效果。

這里的意思其實就是state復用了,但是我們不希望這樣,我們只需要綁定key就可以了。

3.4. StatefulWidget的實現(隨機key)

我們使用一個隨機的key,ListItemFul的修改如下:

class ListItemFul extends StatefulWidget {
  final String name;
  ListItemFul(this.name, {Key key}): super(key: key);
  @override
  _ListItemFulState createState() => _ListItemFulState();
}

home界面代碼修改如下:

body: ListView(
  children: names.map((name) {
    return ListItemFul(name, key: ValueKey(Random().nextInt(10000)),);
  }).toList(),
),

這一次我們發(fā)現,每次刪除都會出現隨機顏色的現象:

  • 這是因為修改了key之后,Element會強制刷新,那么對應的State也會重新創(chuàng)建,這顯然也不是我們想要的效果。
// Widget類中的代碼
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}

3.5. StatefulWidget的實現(name為key)

這次,我們將name作為key來看一下結果:

body: ListView(
  children: names.map((name) {
    return ListItemFul(name, key: ValueKey(name));
  }).toList(),
),

達到了我們理想中的效果。

  • 因為這是在更新widget的過程中根據key進行了diff算法
  • 在前后進行對比時,發(fā)現bbb對應的Element和ccc對應的Element會繼續(xù)使用,那么就會刪除之前aaa對應的Element,而不是直接刪除最后一個Element

總結:key的作用

  1. 指定name為key之后,進行diff算法的時候, 我們就可以指定刪除哪個widget
  2. 我們使用隨意的key,就可以實現強制刷新widget
  3. 開發(fā)中我們使用最多的就是指定name為key之后,達到復用的目的

3.6. Key的分類

Key本身是一個抽象,不過它也有一個工廠構造器,創(chuàng)建出來一個ValueKey,但是我們很少用ValueKey,我們一般用它的子類。

直接子類主要有:LocalKey和GlobalKey

  • LocalKey,它應用于具有相同父Element的Widget進行比較,也是diff算法的核心所在;
  • GlobalKey,通常我們會使用GlobalKey某個Widget對應的Widget或State或Element

3.6.1. LocalKey

LocalKey有三個子類

ValueKey:

  • ValueKey是當我們以特定的值作為key時使用,比如一個字符串、數字等等

ObjectKey:

  • 如果兩個學生,他們的名字一樣,使用name作為他們的key就不合適了
  • 我們可以創(chuàng)建出一個學生對象,使用對象來作為key

UniqueKey:

  • 使用隨機數可以能隨機到一樣的數字,如果我們要確保key的唯一性,可以使用UniqueKey,它的本質是生成一個hashCode;
  • 比如我們之前使用隨機數來保證key的不同,這里我們就可以換成UniqueKey;

3.6.2. GlobalKey

GlobalKey可以幫助我們訪問某個Widget的信息,包括Widget或State或Element等對象。

我們來看下面的例子:我希望可以在HYHomePage中直接訪問HYHomeContent中name和_HYHomeContentState中的message。

直接看注釋的步驟:

class HYHomePage extends StatelessWidget {
  // 1. 創(chuàng)建一個GlobalKey
  final GlobalKey<_HYHomeContentState> homeKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("列表測試"),
      ),
      // 3. 將homeKey傳進去
      body: HYHomeContent(key: homeKey),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.data_usage),
        onPressed: () {
          // 4. 拿到_HYHomeContentState里面的message
          print(homeKey.currentState.message); // abc
          // 5. 調用_HYHomeContentState里面的test方法
          homeKey.currentState.test() // test方法
          // 6. 拿到HYHomeContent里面的name
          print(homeKey.currentState.widget.name); // coderwhy
        },
      ),
    );
  }
}

class HYHomeContent extends StatefulWidget {
  final String name = "coderwhy";

  // 2. 定義構造方法
  HYHomeContent({Key key}): super(key: key);

  @override
  _HYHomeContentState createState() => _HYHomeContentState();
}

class _HYHomeContentState extends State<HYHomeContent> {
  final String message = "abc";
  
  void test() {
    print("test方法")
  }

  @override
  Widget build(BuildContext context) {
    return Text(message);
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容