Flutter 是如何渲染的?

冷石_發表於2020-04-09

前言

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

當應用啟動時 Flutter 會遍歷並建立所有的 Widget 形成 Widget Tree,同時與 Widget Tree 相對應,通過呼叫 Widget 上的 createElement() 方法建立每個 Element 物件,形成 Element Tree

最後呼叫 ElementcreateRenderObject() 方法建立每個渲染物件,形成一個 Render Tree

然後需要知道 WidgetElementRenderObject 到底是啥以及它們是幹什麼的。

什麼是 Widget

Widget 是 Flutter 的核心部分,是使用者介面的不可變描述資訊。正如 Flutter 的口號 Everything’s a widget, 用 Flutter 開發應用就是在寫 Widget ?。

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

@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 配置資料生成。

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

relationship

Widget 變化時,如果兩個 WidgetruntimeTypekey 屬性相同的,那麼新的 Element 會通過 Element.update() 更新舊的 Element,否則舊的 Element 會被刪除,新生成的 Element 插入到樹中。

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系統

部落格地址

相關文章