Flutter 是如何渲染的?

前言

要解答這個問題,首先須要認識到 Flutter 中有三棵樹:Widget 樹,Element 樹和 RenderObject 樹。html

當應用啓動時 Flutter 會遍歷並建立全部的 Widget 造成 Widget Tree,同時與 Widget Tree 相對應,經過調用 Widget 上的 createElement() 方法建立每一個 Element 對象,造成 Element Tree瀏覽器

最後調用 ElementcreateRenderObject() 方法建立每一個渲染對象,造成一個 Render Treemarkdown

而後須要知道 WidgetElementRenderObject 究竟是啥以及它們是幹什麼的。ide

什麼是 Widget

Widget 是 Flutter 的核心部分,是用戶界面的不可變描述信息。正如 Flutter 的口號 Everything’s a widget, 用 Flutter 開發應用就是在寫 Widget 🐶。佈局

Flutter 的 Widget 不僅表示 UI 控件,還表示一些功能性的組件,如路由跳轉 Navigator,手勢檢測 GestureDetector 組件等。post

@immutable
abstract class Widget extends DiagnosticableTree {
  /// Initializes [key] for subclasses.
  const Widget({ this.key });
  final Key key;

  /// ...

  @protected
  Element createElement();

  /// ...

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
  }
}
複製代碼

WidgetcanUpdate 方法經過比較新部件和舊部件的 runtimeTypekey 屬性是否相同來決定更新部件對應的 Element性能

什麼是 Element

Element 是實例化的 Widget 對象,經過 WidgetcreateElement() 方法,在特定位置使用 Widget 配置數據生成。ui

Element 用於管理應用 UI 的更新和更改,管理部件的生命週期,每一個 Element 都包含對 WidgetRenderObject 的引用。this

relationship

Widget 變化時,若是兩個 WidgetruntimeTypekey 屬性相同的,那麼新的 Element 會經過 Element.update() 更新舊的 Element,不然舊的 Element 會被刪除,新生成的 Element 插入到樹中。spa

abstract class Element extends DiagnosticableTree implements BuildContext {
  /// Creates an element that uses the given widget as its configuration.
  ///
  /// Typically called by an override of [Widget.createElement].
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;

  /// Change the widget used to configure this element.
  ///
  /// The framework calls this function when the parent wishes to use a
  /// different widget to configure this element. The new widget is guaranteed
  /// to have the same [runtimeType] as the old widget.
  ///
  /// This function is called only during the "active" lifecycle state.
  @mustCallSuper
  void update(covariant Widget newWidget) {
    /// ...
  }

  /// Creates an instance of the [RenderObject] class that this
  /// [RenderObjectWidget] represents, using the configuration described by this
  /// [RenderObjectWidget].
  ///
  /// This method should not do anything with the children of the render object.
  /// That should instead be handled by the method that overrides
  /// [RenderObjectElement.mount] in the object rendered by this object's
  /// [createElement] method. See, for example,
  /// [SingleChildRenderObjectElement.mount].
  @protected
  RenderObject createRenderObject(BuildContext context);
}
複製代碼

什麼是 RenderObject

RenderObject 用於應用界面的佈局和繪製,保存了元素的大小,佈局等信息,實例化一個 RenderObject 是很是耗能的。

當應用運行時 Flutter 使用 RenderObject 的數據繪製應用界面,最終造成一個 Render Tree

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  /// Initializes internal fields for subclasses.
  RenderObject() {
    _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
  }

  /// The render object at (or below) this location in the tree.
  ///
  /// If this object is a [RenderObjectElement], the render object is the one at
  /// this location in the tree. Otherwise, this getter will walk down the tree
  /// until it finds a [RenderObjectElement].
  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;
  }

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    /// ...
  }

  /// ...

  void paint(PaintingContext context, Offset offset) {
    /// ...
  }

}
複製代碼

爲何須要三棵樹

使用三棵樹的目的是儘量複用 Element

複用 Element 對性能很是重要,由於 Element 擁有兩份關鍵數據:Stateful widget 的狀態對象及底層的 RenderObject

當應用的結構很簡單時,或許體現不出這種優點,一旦應用複雜起來,構成頁面的元素愈來愈多,從新建立 3 棵樹的代價是很高的,因此須要最小化更新操做。

當 Flutter 可以複用 Element 時,用戶界面的邏輯狀態信息是不變的,而且能夠重用以前計算的佈局信息,避免遍歷整棵樹。

舉個例子說明

建立一個簡單的 Flutter 應用,代碼以下

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      color: Colors.white,
      debugShowCheckedModeBanner: false,
      builder: (context, child) => HomePage(),
    ),
  );
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _isWorld = true;

  Widget _buildWorld() {
    return RichText(
      text: TextSpan(
        text: 'Hello world',
        style: TextStyle(color: Colors.black),
      ),
    );
  }

  Widget _buildFlutter() {
    return RichText(
      text: TextSpan(
        text: 'Hello flutter',
        style: TextStyle(color: Colors.black),
      ),
    );
  }

  void changeText() {
    setState(() {
      _isWorld = !_isWorld;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Center(
            child: _isWorld ? _buildWorld() : _buildFlutter(),
          ),
          SizedBox(height: 20.0),
          // Padding(padding: EdgeInsets.only(top: 20.0)),
          IconButton(icon: Icon(Icons.refresh), onPressed: changeText)
        ],
      ),
    );
  }
}

複製代碼

顯示效果

simulator-world

打開 Dart DevTools,能夠看到應用的 Widget Tree,此時 RichText 控件的 RenderObject 的 ID 是 #6276a

world-id

點擊圖標將文字變成 Hello flutter

simulator-flutter

刷新瀏覽器頁面再次查看 RichTextRenderObject 的 ID 依然是 #6276a

flutter-id

能夠發現 Flutter 只是更新了文字數據,複用了 RichText 對應的 ElementRenderObject

而使用 SizedBox 部件取代 Padding 部件時。

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Center(
          child: RichText(
            text: TextSpan(
              text: 'Hello $text',
              style: TextStyle(color: Colors.black),
            ),
          ),
        ),
        SizedBox(height: 20.0),
        // Padding(padding: EdgeInsets.only(top: 20.0)),
        IconButton(icon: Icon(Icons.refresh), onPressed: changeText)
      ],
    ),
  );
}
複製代碼

padding

Padding 部件對應的 ElementRenderObject 都會被從樹中移除,使用 SizedBox 新生成的替代。

sizeedbox

總結

Widget 是應用界面的聲明信息。 Element 連接 WidgetRenderObject,管理界面的更新和修改。 RenderObject 保存具體的佈局信息,負責繪製 UI。

widget-element-render-object

參考

How Flutter renders Widgets (Video)

How Flutter renders Widgets

Flutter UI系統

博客地址

相關文章
相關標籤/搜索