Flutter UI 渲染原理概览
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter UI 渲染原理概览相关的知识,希望对你有一定的参考价值。
三棵树
什么是三棵树? 在 Flutter 中 Widget 是核心,一切都是 Widget,但一起协同工作的还有另外两个元素:Element 和 RenderObject。由于它们都是有着树形结构,所以经常会称它们为三棵树。
Widget
在开发 Flutter 应用过程中,接触最多的无疑就是Widget,是『描述』 Flutter UI 的基本单元。Flutter 在 widgets 层中使用了相同的概念(一个 Widget)来表示屏幕上的绘制、布局(位置和大小)、用户交互、状态管理、主题、动画及导航。
Google 在设计 Widget 时,还赋予它一些鲜明的特点:
- 声明式 UI —— 相对于传统 Native 开发中的命令式 UI,声明式 UI 有不少优势,如:开发效率显著提升、UI 可维护性明显加强等;
- 不可变性 —— Flutter 中所有 Widget 都是不可变的(immutable),即其内部成员都是不可变的(final),对于变化的部分需要通过「Stateful Widget-State」的方式实现;
- 组合大于继承 —— Widget 设计遵循组合大于继承这一优秀的设计理念,再小的功能也能被抽象,通过组合实现复杂的功能。
Widget 从功能上可以分为 3 类:「Component Widget」、「Proxy Widget」以及「Renderer Widget」,但只有 Renderer Widget 会转换为 Render Object 最终显示在 UI 上。
在 Widget 注释开头就可以看到:
/// Describes the configuration for an [Element].
///
/// Widgets are the central class hierarchy in the Flutter framework. A widget
/// is an immutable description of part of a user interface. Widgets can be
/// inflated into elements, which manage the underlying render tree.
///
这段注释阐明了Widget的本质:用于配置 Element 的,Widget 本质上是 UI 的配置信息 (附带部分业务逻辑)。
Element
我们知道 Widget 本质上是 UI 的配置数据 (静态、不可变),Element 则是通过 Widget 生成的『实例』,两者间的关系就像是 JSON 与 Object 。同一份配置 (Widget) 可以生成多个实例 (Element),这些实例可能会被安插在树上不同的位置。
同样,Element 注释中第一句话也可以看出:
/// An instantiation of a [Widget] at a particular location in the tree.
///
/// Widgets describe how to configure a subtree but the same widget can be used
/// to configure multiple subtrees simultaneously because widgets are immutable.
/// An [Element] represents the use of a widget to configure a specific location
/// in the tree. Over time, the widget associated with a given element can
/// change, for example, if the parent widget rebuilds and creates a new widget
/// for this location.
///
Element 间实际上有一棵真实存在的树「Element Tree」,Element 有 2 个主要职责:
- 根据 UI (「Widget Tree」) 的变化来维护「Element Tree」,包括:节点的插入、更新、删除、移动等;
- Widget 与 RenderObject 间的协调者。(它内部持有 Widget 和 RenderObject 的引用)
「Component Element」 —— 组合型 Element,「Component Widget」、「Proxy Widget」对应的 Element 都属于这一类型,其特点是子节点对应的 Widget 需要通过build方法去创建。同时,该类型 Element 都只有一个子节点 (single child); 「Renderer Element」 —— 渲染型 Element,对应「Renderer Widget」,其不同的子类型包含的子节点个数也不一样。
RenderObject
官方文档中有一段话
//In android, the View is the foundation of everything that shows up on the screen. Buttons, toolbars, and
//inputs, everything is a View. In Flutter, the rough equivalent to a View is a Widget. Widgets don’t map
//exactly to Android views, but while you’re getting acquainted with how Flutter works you can think of them
//as “the way you declare and construct UI”.
意思是,Widget 可以粗糙地看做 Android 中的 View,因为他们都可以描述 UI,但准确来说他们是不相等的。
实际上,RenderObject 更像是 Android 中的 View, RenderObject 是实际用来参与绘制的对象。它有和 View 类似的Layout
Paint
Composite
渲染流程,包括很多方法也一致。
Flutter | RenderObject | Android View |
---|---|---|
绘制 | paint() | draw()/onDraw() |
布局 | performLayout()/layout() | measure()/onMeasure(), layout()/onLayout() |
布局约束 | Constraints | MeasureSpec |
布局协议1 | performLayout() 的 Constraints 参数表示父节点对子节点的布局限制 | measure() 的两个参数表示父节点对子节点的布局限制 |
布局协议2 | performLayout() 应调用各子节点的 layout() | onLayout() 应调用各子节点的 layout() |
布局参数 | parentData | mLayoutParams |
请求布局 | markNeedsLayout() | requestLayout() |
请求绘制 | markNeedsPaint() | invalidate() |
添加 child | adoptChild() | addView() |
移除 child | dropChild() | removeView() |
关联到窗口/树 | attach() | onAttachedToWindow() |
从窗口/树取消关联 | detach() | onDetachedFromWindow() |
获取 parent | parent | getParent() |
触摸事件 | hitTest() | onTouch() |
用户输入事件 | handleEvent() | onKey() |
旋转事件 | rotate() | onConfigurationChanged() |
当然,RenderObject 也只是抽象类,描述了渲染的基本协议,一些具体的实现比如坐标系都没有定义。 Flutter 原生提供的实现是 RenderBox ,大部分 widgets 都是基于它实现的。
渲染过程
构建 Widgets
首先观察以下的代码片段,它代表了一个简单的 widget 结构:
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
Text('A'),
],
),
);
当 Flutter 需要绘制这段代码片段时,框架会调用 build() 方法,返回一棵基于当前应用状态来绘制 UI 的 widget 子树。在这个过程中,build() 方法可能会在必要时,根据状态引入新的 widget。在上面的例子中,Container 的 color 和 child 就是典型的例子。我们可以查看 Container 的 源代码,你会看到当 color 属性不为空时,ColoredBox 会被加入用于颜色布局。
if (color != null)
current = ColoredBox(color: color!, child: current);
与之对应的,Image 和 Text 在构建过程中也会引入 RawImage 和 RichText。如此一来,最终生成的 widget 结构比代码表示的层级更深,在该场景中如下图2:
这就是为什么你在使用 Dart DevTools 的 Flutter inspector 调试 widget 树结构时,会发现实际的结构比你原本代码中的结构层级更深。
从 Widget 到 Element
在构建的阶段,Flutter 会将代码中描述的 widgets 转换成对应的 Element 树,每一个 Widget 都有一个对应的 Element。每一个 Element 代表了树状层级结构中特定位置的 widget 实例。目前有两种 Element 的基本类型:
-
ComponentElement
,其他 Element 的宿主。 -
RenderObjectElement
,参与布局或绘制阶段的 Element。
RenderObjectElement
是底层 RenderObject
与对应的 widget 之间的桥梁。
任何 widget 都可以通过其 BuildContext 引用到 Element,它是该 widget 在树中的位置的上下文。类似 Theme.of(context) 方法调用中的 context,它作为 build() 方法的参数被传递。
由于 widgets 以及它上下节点的关系都是不可变的,因此,对 widget 树做的任何操作(例如将 Text(‘A’) 替换成 Text(‘B’))都会返回一个新的 widget 对象集合。但这并不意味着底层呈现的内容必须要重新构建。 Element 树每一帧之间都是持久化的,因此起着至关重要的性能作用, Flutter 依靠该优势,实现了一种好似 widget 树被完全抛弃,而缓存了底层表示的机制。 Flutter 可以根据发生变化的 widget,来重建需要重新配置的 Element 树的部分。
布局和渲染
很少有应用只绘制单个 widget。因此,有效地排布 widget 的结构及在渲染完成前决定每个 Element 的大小和位置,是所有 UI 框架的重点之一。
在渲染树中,每个节点的基类都是 RenderObject
,该基类为布局和绘制定义了一个抽象模型。这是再平凡不过的事情:它并不总是一个固定的大小,甚至不遵循笛卡尔坐标规律(根据该 极坐标系的示例 所示)。每一个 RenderObject
都了解其父节点的信息,但对于其子节点,除了如何 访问 和获得他们的布局约束,并没有更多的信息。这样的设计让 RenderObject
拥有高效的抽象能力,能够处理各种各样的使用场景。
在构建阶段,Flutter 会为 Element 树中的每个 RenderObjectElement
创建或更新其对应的一个从 RenderObject
继承的对象。 RenderObject
实际上是原语:渲染文字的 RenderParagraph
、渲染图片的 RenderImage
以及在绘制子节点内容前应用变换的 RenderTransform
是更为上层的实现。
更新 UI
上面介绍了 Flutter 在 Framework 级别渲染的流程(后续交给图像引擎),这只是一帧的流程。Flutter 在更新 UI 上也有一些不同。
更新 Widget Tree
之前提到了 Widget 的不可变性,所以每一帧都会调用 build()
方法返回 widget 树,即使是 StatefulWidget ,也只是根据不同的状态返回不同的 widget 树 。但这样的话理论上会有大量的实例的产生和销毁,频繁 GC, 不过实际上,上面提到了 Widget 只是 UI 的配置,不负责实际的渲染,开销并没有那么大。
更新 Element Tree
更重量级的 Element Tree 并不会全部重新渲染,而是根据 Widget Tree 的变化维护 Element Tree (插入、删除、更新、移动…),其中核心的几个方法包括:
1.Element 调用 Widget.canUpdate()
,去判断新的 widget 是否能用于更新 Element。
static bool canUpdate(Widget oldWidget, Widget newWidget)
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
2.可以更新的话调用 Element.update()
Element.updateChild()
…
//继承类自行实现
@mustCallSuper
void update(covariant Widget newWidget)
_widget = newWidget;
//framework.dart
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot)
if (newWidget == null)
if (child != null)
deactivateChild(child);
return null;
Element newChild;
if (child != null)
assert(()
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
());
if (hasSameSuperclass && child.widget == newWidget)
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget))
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(()
child.owner._debugElementWasRebuilt(child);
return true;
());
newChild = child;
else
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot);
else
newChild = inflateWidget(newWidget, newSlot);
assert(()
if (child != null)
_debugRemoveGlobalKeyReservation(child);
final Key key = newWidget?.key;
if (key is GlobalKey)
key._debugReserveFor(this, newChild);
return true;
());
return newChild;
...
static bool canUpdate(Widget oldWidget, Widget newWidget)
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
- newWidget == null —— 说明子节点对应的 Widget 已被移除,直接 remove child element (如有);
- child == null —— 说明 newWidget 是新插入的,创建子节点 (inflateWidget);
- child != null —— 此时,分为 3 种情况:
- 若 child.widget == newWidget,说明 child.widget 前后没有变化,若 child.slot != newSlot 表明子节点在兄弟结点间移动了位置,通过updateSlotForChild修改 child.slot 即可;
- 通过Widget.canUpdate判断是否可以用 newWidget 修改 child element,若可以,则调用update方法;
- 否则先将 child element 移除,并通 newWidget 创建新的 element 子节点。
更新 RenderObject Tree
…
Dart VM
DartVM 的内存回收机制采用多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化。 基本的流程: DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内存段的过程:
Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速分配。 Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存:
整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种算法也非常适合Flutter框架中大量Widget重建的场景。
以上是关于Flutter UI 渲染原理概览的主要内容,如果未能解决你的问题,请参考以下文章