1
头图

Flutter 中,我们写的各种 Widget,如 Text, Padding, Image 等是如何转化成像素显示在屏幕上的呢?

今天我们来讲讲 Flutter 中的 Widget, Element, RenderObject,也就是我们常说的三棵树,以及 Flutter 是如何将 Widgets 渲染至屏幕上的。

Widget

Describes the configuration for an Element.A widget is an immutable description of part of a user interface (link).

Widget 是描述 UI 元素的配置信息,可以理解为前端开发中的控件或者组件,是 Flutter 最基本的概念。所谓元素配置信息就是这些 Widget 类所接受的参数,比如对于 Text 来讲,文本内容、样式、对齐方式都是它的配置信息。

Text(
  "Hello wolrd",
  style: TextStyle(
    fontSize: 16,
    color: Colors.red,
  ),
  textAlign: TextAlign.center,
)

我们在代码中写的 Text, Row, Padding 等都是 Flutter 内置的 Widget,我们平时就是用这些内置的 Widget 来“搭建”页面的,这些 Widget 就构成了一棵 Widget 树。

Flutter 中的 Widget 最终都是继承自 Widget 接口。

image.png

从源码中可以看到有 @immutable 注解,表明 Widget 是不可变的,(当配置信息发生变化时,Flutter 会选择重新构建 Widget 树方式来进行数据更新。)

那么 Flutter 是如何实现动态化的呢?我们接着来看 ElementRenderObject

Element

An instantiation of a Widget at a particular location in the tree.

ElementWidget 的一个实例化对象,承载中视图构建的上下文(context)数据,是连接配置信息到最终渲染的桥梁。这些 Element 实例是可变的,可以与 RenderObject 进行通信。

image.png

RenderObject

An object in the render tree.

顾名思义,RenderObject 主要负责实现视图渲染的对象。渲染时所涉及到的尺寸、布局、约束条件等都是由它来控制的。因此,RenderObject 实例化成本非常昂贵。和 Widget, Element 的一样,Flutter 程序中也有一棵 RenderObject 树。

注:RenderObject 是个总称,不同的 Widget 会有不同类型的 RenderObject,如 RenderOpacity, RenderParagraph, RenderImage 等,它们都继承自 RenderObject

为了方便理解,我们可以与前端中的框架做个简单的类比。

  • Vue: Template → Virtual DOM → DOM
  • React: JSX → Virtual DOM → DOM
  • React Native: JSX → Virtual DOM → Android/iOS 原生控件
  • Flutter: Widget → Element → RenderObject

渲染过程

接下来,我们来探究一下,Flutter 是如何使用 Widget, Element, RenderObject 来完成渲染流程的。我们以一个例子来说明。

void main() {
  runApp(
    RichText(
      text: const TextSpan(
        text: 'Hello World',
        style: TextStyle(color: Colors.red),
      ),
      textDirection: TextDirection.ltr,
    ),
  );
}

当执行到 runApp

void runApp(Widget app) {
  final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
  assert(binding.debugCheckZone('runApp'));
  binding
    ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
    ..scheduleWarmUpFrame();
}

首先要做的是将 RichText 添加至 Widget 树中。

image.png

然后,Flutter 会根据 Widget 树会创建 Element 树, 用来管理 Widget 树生命周期和状态。

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
  /// Initializes fields for subclasses.
  const MultiChildRenderObjectWidget({ super.key, this.children = const <Widget>[] });

  final List<Widget> children;

  @override
  MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}

现在已经存在 WidgetElement 两棵树了。

image.png

接着,Element 会在挂载的时候创建 RenderObject

@override
void mount(Element? parent, Object? newSlot) {
  super.mount(parent, newSlot);
  // ...
  _renderObject = (widget as RenderObjectWidget).createRenderObject(this);
  // ...
  attachRenderObject(newSlot);
  super.performRebuild(); // clears the "dirty" flag
}

当我们书写 RichText(...) 时,会将里面的配置信息(各种参数)传入至 RenderObject(这里是 RenderParagraph) 中

@override
RenderParagraph createRenderObject(BuildContext context) {
  assert(textDirection != null || debugCheckHasDirectionality(context));
  return RenderParagraph(text,
    textAlign: textAlign,
    textDirection: textDirection ?? Directionality.of(context),
    softWrap: softWrap,
    overflow: overflow,
    textScaler: textScaler,
    maxLines: maxLines,
    strutStyle: strutStyle,
    textWidthBasis: textWidthBasis,
    textHeightBehavior: textHeightBehavior,
    locale: locale ?? Localizations.maybeLocaleOf(context),
    registrar: selectionRegistrar,
    selectionColor: selectionColor,
  );
}

现在,Widget, Element, RenderObject 就都有了。

image.png

最终,RenderParagraph 会根据 RichText 中所声明的各种配置信息进行绘制,最终显示在屏幕上。

我们来总结一下整个渲染过程:

  • 首先,通过 Widget 树生成对应的 Element 树;
  • 然后,Element 挂载时创建 RenderObject,并关联至 Element.renderObject 属性上;
  • 最后,RenderObject 树完成最终的渲染。

可以看出,ElementWidgetRenderObject 的桥梁,那可不可以不要 Element 呢?

为了理解这个,我们来看看当 Widget 发生变化时的情况。

void main() {
  runApp(
    RichText(
      text: const TextSpan(
        text: 'Hello World',
        style: TextStyle(color: Colors.red),
      ),
      textDirection: TextDirection.ltr,
    ),
  );
  runApp(
    RichText(
      text: const TextSpan(
        text: 'Hello Shanghai',
        style: TextStyle(color: Colors.red),
      ),
      textDirection: TextDirection.ltr,
    ),
  );
}

我们连续执行两遍 runApp 函数来模拟更新的情况。

第一次执行 runApp 的时候会创建三棵树,第二次执行的时候,text 发生了变化

image.png

这时,canUpdate 会执行。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}这个方法用来决定是重新创建 Element,还是复用之前的 Element,复用时执行 updateRenderObject 方法@override
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
  assert(textDirection != null || debugCheckHasDirectionality(context));
  renderObject
    ..text = text
    ..textAlign = textAlign
    ..textDirection = textDirection ?? Directionality.of(context)
    ..softWrap = softWrap
    ..overflow = overflow
    ..textScaler = textScaler
    ..maxLines = maxLines
    ..strutStyle = strutStyle
    ..textWidthBasis = textWidthBasis
    ..textHeightBehavior = textHeightBehavior
    ..locale = locale ?? Localizations.maybeLocaleOf(context)
    ..registrar = selectionRegistrar
    ..selectionColor = selectionColor;
}

可以看出,这与创建 RenderObject 非常相似,只是复用了 RenderObject,仅更新其值,而没有重新创建新的 RenderObject(RenderParagraph)。

image.png

最后,我们看到,当配置信息发生变化时,真正用来渲染的 RenderObject 并没有变化。

前面说到 Widget 具有不可变性,体现在当其发生变化时,Flutter 会直接丢弃原有的 Widget 树,重新构建一棵新的 Widget树,因为只是一份轻量的数据结构,并不参与最终的渲染,重建的成本很低。这也是为什么 Widget 中的属性必须是 final 的原因。

Element 却是可变的,实际上,Element 树这一层就是将 Widget 树中的变化做了抽象,将真正修改的部分 patch 到真实的 RenderObject 树中,最大程度了降低对 RenderOject 树的修改,提高渲染效率,而不是销毁整个渲染树重建。这就是 Element 存在的意义。

接下来,我们用个例子来验证一下我们的结论。

在这个例子中,当点击按钮时,图片和文本都会发生变化,两张图片的尺寸是不一样的。如果上述分析是正确的,当屏幕内容发生变化时,Flutter 会尽可能地复用 RenderObject,也就是说变化前后 ImageText 对应 RenderObject 的 ID 应该保持不变。我们用 Flutter 开发者工具来查看 Widget 树和它的详情。


RenderObject ID for dart-logo image (ID = RenderImage#14fe4)


RenderObject ID for flutter-logo image (ID = RenderImage#14fe4)

我们可以看到,用于渲染图片的 RenderObject (RenderImage) 前后的 ID 保持不变,这就意味着两张图片都是由同一个 RenderObject 来绘制的,说明元素得到了复用。


RenderObject ID for the text (ID = RenderParagraph#19921)


RenderObject ID for the text (ID = RenderParagraph#19921)

同理也发生在 Text,尽管文本发生了变化,但是 RenderObject 得到了复用,RenderParagraph 的 ID 保持不变。正是这种复用的能力,使得我们 Widget 树不断变化时能够快速地反映至屏幕上, 这也是 Flutter 表现出色的原因之一。


见贤思齐
66 声望8 粉丝

写代码的