Flutter 渲染原理

Flutter 三棵树

Flutter 在内部构建了三棵树,分别为 Widgets Tree、Element Tree、Render Tree。它们长这样:

img

Widgets Tree

widget 是声明式 UI 的写法,它描述程序运行后界面应该呈现什么内容。

我们在代码中写的各种 Widget 运行后就构成了 Widgets Tree ,它们是一层层的嵌套关系,几乎所有我们用到的 widget 都具有这种层级关系。

官方文档对 Widgets 是这样描述的: Widget 是用户界面的一部分的不可变描述。

不可变的这一特点非常重要,这意味着用户界面上的任何修改都需要重新构建新的 widget。

三棵树的概念由此概念发挥作用,在渲染引擎下,flutter 利用三种树的实例,它们中的每一个都有略微不同的用途,但它们结合在一起可以实现快速的用户界面渲染和出色的性能。

Elements Tree

与 Widgets 相比,Elements 是可变的。这意味着可以更改元素内的属性,而无需创建新元素。官方文档对 Element 的定义是:"在树中特定位置的 widget 的实例化"。

Element 负责更新用户界面,它是 介于 Widget 和 Render Object 之间的一种链接。

Render Object Tree

Render Object(渲染对象)是构建用户界面部分的信息来源,Flutter 并不是查看 Widgets Tree,而是 Render Object。一个 RenderObject 的实例包含 Widget 内的所有关于 Size、painting 和 layout 的信息。这使得创建成本非常高,因此 Flutter 仅在必要时尝试创建新的 Render Object。

小结

Flutter 运行时,内部会维护着三棵树,分别是 Widgets Tree、Elements Tree 和 RenderObject Tree。它们的关系可以用一幅图表示:

img

三棵树基本运作原理

我们写的代码构成 Widgets Tree,最终 Widgets Tree 会被解析成 RenderObject Tree。

RenderObject tree 是渲染树,里面有一个个的 RenderObject,整个 RenderObject 层是渲染库的核心。flutter 引擎读取上面的信息,根据上面的信息来布局、绘制。

那么 Element Tree 存在的意义是什么呢?

Widgets 树有一个特点,它会随着 state 的改变而被重新 build,当用户在界面中触发某个行为改变了状态后,会重新触发 build 方法并重构 widget。

这个特点使得 Widgets 树非常不稳定,因为很小的改动都可能导致整棵树重新 build,不难想象,一点小改动使得页面中不必更新的部分也跟着重新渲染会造成极大的性能浪费。

为了解决这个问题,flutter 借鉴了 React 虚拟 dom 的思想,在中间加了一层 Element tree。Elements tree 是用来描述 widget 树结构的对象。

Element tree 的好处是它是一个比较稳定的数据结构,当状态发生更新后,flutter 将旧的 widgets tree 跟新的 widgets tree 做对比,这个对比方法是 diff 算法,diff 取自 different 。

当新的跟旧的 widget tree 对比后,会有一个结果,这个结果叫 patch(补丁),最后用 patch 修改 Element tree 中需要变更的内容,最终 Element tree 被修改的部分映射到 Render tree 上也将是极小的性能开销。

这就是 flutter 中三棵树基本的运作原理。

源码中隐含着更多细节

某些 widget 能够直接生成 RenderObject

以 Padding 为例,我们可以通过查看 Padding 的源码查看到 Padding 的继承关系

dart
1Padding => SingleChildRenderObjectWidget => RenderObjectWidget => Widget

在RenderObjectWidget 中,有一个createRenderObject方法,它是这样的:

dart
1 RenderObject createRenderObject(BuildContext context);

由于RenderObjectWidget是抽象类,所以这个方法必须被子类所实现。

RenderObjectWidget的子类是SingleChildRenderObjectWidget,但是它也是抽象类,它的方法会被 Padding 实现,以下是Padding对于createRenderObject方法的具体实现:

dart
1  
2  RenderPadding createRenderObject(BuildContext context) {
3    return RenderPadding(
4      padding: padding,
5      textDirection: Directionality.maybeOf(context),
6    );
7  }

RenderObject是一个类,我们顺着它往上找,可以得出这样的关系链

dart
1RenderPadding => RenderShiftedBox => RenderBox => RenderObject

最终我们可以得出 Padding 这个类,是可以直接构建RenderObject的。

所有 widget 都会创建 Element

每个 widget(或者其父类)都有createElement的方法,以我们经常写的StatelessWidget为例,它的内部就有这个方法

dart
1 StatelessElement createElement() => StatelessElement(this);

如果顺着StatelessWidget的父类往上找,可以得出 createElement方法是抽象类Widget的抽象方法:

dart
1	
2  
3  Element createElement();

所有 widget 都继承自 Widget,所以所有 Widget 都必然有这个方法,那么也表明所有 widget 都能创建 Element。

创建 Element 后,会调用 mount 方法

从StatelessWidget这个抽象类中,我们可以看到有个普通方法:

dart
1  
2  StatelessElement createElement() => StatelessElement(this);

顺着StatelessElement这个类往上找,可以得出以下顺序:

dart
1StatelessElement => ComponentElement => Element

在 ComponentElement 这个类中,可以看到 mount 方法

dart
1  
2  void mount(Element? parent, Object? newSlot) {
3    super.mount(parent, newSlot);
4    assert(_child == null);
5    assert(_lifecycleState == _ElementLifecycle.active);
6    _firstBuild();
7    assert(_child != null);
8  }
9
10  void _firstBuild() {
11    // StatefulElement overrides this to also call state.didChangeDependencies.
12    rebuild(); // This eventually calls performRebuild.
13  }

这个 mount 方法调用了super.mount方法,并执行了_firstBuild,而_firstBuild方法则调用了 rebuild,进入rebuild后能看到它调用了performRebuild()方法,performRebuild是抽象方法,通过查看他的实现可以看到源码:

image-20220329141832373

在源码中可以看到它调用了 build 方法,接着查找 build 方法的实现,最终能够看到如下代码:

image-20220329141955589

这时候就可以知道原来 statelessElement 会调用 widget 中的 build 方法。

整个过程就是这样的:

  1. StatelessWidget 调用createElement 方法,返回 实例 StatelessElement(this)
  2. StatelessElement的父类调用 mount 方法,接着调用 firstBuild
  3. 最终firstBuild会调用 widget 上的 build 方法

那么这个 mount 有什么用?它主要是用来挂载 Element 到 tree 树上的。

在 Element 里,我们也能看到 mount 方法,并且还有一段注释

image-20220328223718577

注释的意思是会将 Element 挂载到树的给定父级的给定插槽中。当新创建的元素首次添加到树中时,框架将调用此函数。

结论:框架会调用 mount 方法,最终会调用到 widget.build 方法将结果挂载到 Element tree 上

为什么能访问 widget.xxx

当我们使用statefulWidget时,经常会写createState方法并返回一个实例

dart
1class HomeContent extends StatefulWidget {
2  final name = 'my name';
3  const HomeContent({Key? key}) : super(key: key);
4
5  
6  State<HomeContent> createState() => _HomeContentState();
7}
8
9class _HomeContentState extends State<HomeContent> {
10  final List<Hotkey> hotKeyList = [];
11
12  
13  Widget build(BuildContext context) {
14     // widget.xxx 这里能够访问到上面的 name
15    final name = widget.name;
16		// ...return something
17  }
18}

为什么能够在访问到上面的 name 呢?这需要我们查看createState方法做了什么。

这里的 createState方法实际上是由StatefulWidget抽象类中的createElement给调用了。

dart
1abstract class StatefulWidget extends Widget {
2  /// Initializes [key] for subclasses.
3  const StatefulWidget({ Key? key }) : super(key: key);
4
5  /// Creates a [StatefulElement] to manage this widget's location in the tree.
6  ///
7  /// It is uncommon for subclasses to override this method.
8  
9  StatefulElement createElement() => StatefulElement(this);
10  // 省略后面全部代码...
11}

它调用 createElement 后返回StatefulElement实例,并把自己传进去了。

让我们看看StatefulElement这个类做了什么

image-20220329150941177

上面的代码括红的部分说明在实例化StatefulElement时用初始化列表的方式调用了传入的 widget(也就是我们写的 widget)的 createState 方法,并把 state 存到 Element 的_state属性中了,接着用一个 get state 返回这个_state属性,并且将传入的 widget 赋值给这个 get state。

dart
1State<StatefulWidget> get state => _state!;
2state._widget = widget;

通过这一系列操作,最后的结果就是我们能够在我们的 state 中通过访问 widget.xxx属性得到HomeContent中的属性。

BuildContext 是什么

在 build 方法中,天然一个参数,他是 BuildContext 类型的。

dart
1class HomePage extends StatelessWidget {
2  final String title;
3  const HomePage({Key? key, required this.title}) : super(key: key);
4
5  
6  Widget build(BuildContext context) {
7    return Scaffold(appBar: AppBar(title: Text(title)), body: HomeContent());
8  }
9}

还是需要通过StatelessWidget中的源代码找

dart
1  
2  StatelessElement createElement() => StatelessElement(this);

在StatelessElement中,我们可以看到它里面有一个 build 方法

dart
1  
2  Widget build() => widget.build(this);

它调用了 widget 的 build 方法,这个 widget 就是我们写的 widget,而 this 就是Element自身,说明 context 就是 Element

key

key 的作用跟 React 的基本差不多,都是为了保证 diff 算法时给一个标记,Widget 这个抽象类中就有相关代码

dart
1  static bool canUpdate(Widget oldWidget, Widget newWidget) {
2    return oldWidget.runtimeType == newWidget.runtimeType
3        && oldWidget.key == newWidget.key;
4  }

它的意思是通过对比 oldWidget 和 newWidget 的类型和 key 来决定要不要 update,如果全部一样,说明 Element并不需要更新,这样就可以复用以前的 Element。这也是 Element tree 稳定的原因。

key 的分类

  • LocalKey 用于相同父 Element 的 Widget 进行比较,也是 diff 算法的核心

    LocalKey 有三个子类:

    • ValueKey 用特定值作为 key 时使用,比如数字、字符串

    • ObjectKey 用对象当 key

    • UniqueKey 需要确保 key 的唯一性时使用

  • GlobalKey 可以帮助我们访问某个 Widget 的信息,包括 Widget 或 State 或 Element 等对象

    我们来看下面的例子:我希望可以在 HYHomePage 中直接访问 HYHomeContent 中的内容

    dart
    1class HYHomePage extends StatelessWidget {
    2  final GlobalKey<_HYHomeContentState> homeKey = GlobalKey();
    3
    4  
    5  Widget build(BuildContext context) {
    6    return Scaffold(
    7      appBar: AppBar(
    8        title: Text("列表测试"),
    9      ),
    10      body: HYHomeContent(key: homeKey),
    11      floatingActionButton: FloatingActionButton(
    12        child: Icon(Icons.data_usage),
    13        onPressed: () {
    14          print("${homeKey.currentState.value}");
    15          print("${homeKey.currentState.widget.name}");
    16          print("${homeKey.currentContext}");
    17        },
    18      ),
    19    );
    20  }
    21}
    22
    23class HYHomeContent extends StatefulWidget {
    24  final String name = "123";
    25
    26  HYHomeContent({Key key}): super(key: key);
    27
    28  
    29  _HYHomeContentState createState() => _HYHomeContentState();
    30}
    31
    32class _HYHomeContentState extends State<HYHomeContent> {
    33  final String value = "abc";
    34
    35  
    36  Widget build(BuildContext context) {
    37    return Container();
    38  }
    39}