You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

09 | Widget构建Flutter界面的基石

你好,我是陈航。

在前面的Flutter开发起步和Dart基础模块中我和你一起学习了Flutter框架的整体架构与基本原理分析了Flutter的项目结构和运行机制并从Flutter开发角度介绍了Dart语言的基本设计思路也通过和其他高级语言的类比深入认识了Dart的语法特性。

这些内容是我们接下来系统学习构建Flutter应用的基础可以帮助我们更好地掌握Flutter的核心概念和技术。

在第4篇文章“Flutter区别于其他方案的关键技术是什么”中我和你分享了一张来自Flutter官方的架构图不难看出Widget是整个视图描述的基础。这张架构图很重要所以我在这里又放了一次。

图1 Flutter架构图

备注:此图引自Flutter System Overview

那么Widget到底是什么呢

Widget是Flutter功能的抽象描述是视图的配置信息同样也是数据的映射是Flutter开发框架中最基本的概念。前端框架中常见的名词比如视图View、视图控制器View Controller、活动Activity、应用Application、布局Layout在Flutter中都是Widget。

事实上,Flutter的核心设计思想便是“一切皆Widget”。所以我们学习Flutter首先得从学会使用Widget开始。

那么在今天的这篇文章中我会带着你一起学习Widget在Flutter中的设计思路和基本原理以帮助你深入理解Flutter的视图构建过程。

Widget渲染过程

在进行App开发时我们往往会关注的一个问题是如何结构化地组织视图数据提供给渲染引擎最终完成界面显示。

通常情况下不同的UI框架中会以不同的方式去处理这一问题但无一例外地都会用到视图树View Tree的概念。而Flutter将视图树的概念进行了扩展把视图数据的组织和渲染抽象为三部分即WidgetElement和 RenderObject。

这三部分之间的关系,如下所示:

图2 WidgetElement与RenderObject

Widget

Widget是Flutter世界里对视图的一种结构化描述你可以把它看作是前端中的“控件”或“组件”。Widget是控件实现的基本逻辑单位里面存储的是有关视图渲染的配置信息包括布局、渲染属性、事件响应信息等。

在页面渲染上,Flutter将“Simple is best”这一理念做到了极致。为什么这么说呢Flutter将Widget设计成不可变的所以当视图渲染的配置信息发生变化时Flutter会选择重建Widget树的方式进行数据更新以数据驱动UI构建的方式简单高效。

这样做的缺点是因为涉及到大量对象的销毁和重建所以会对垃圾回收造成压力。不过Widget本身并不涉及实际渲染位图所以它只是一份轻量级的数据结构重建的成本很低。

另外由于Widget的不可变性可以以较低成本进行渲染节点复用因此在一个真实的渲染树中可能存在不同的Widget对应同一个渲染节点的情况这无疑又降低了重建UI的成本。

Element

Element是Widget的一个实例化对象它承载了视图构建的上下文数据是连接结构化的配置信息到完成最终渲染的桥梁。

Flutter渲染过程可以分为这么三步

  • 首先通过Widget树生成对应的Element树
  • 然后创建相应的RenderObject并关联到Element.renderObject属性上
  • 最后构建成RenderObject树以完成最终的渲染。

可以看到Element同时持有Widget和RenderObject。而无论是Widget还是Element其实都不负责最后的渲染只负责发号施令真正去干活儿的只有RenderObject。那你可能会问既然都是发号施令那为什么需要增加中间的这层Element树呢直接由Widget命令RenderObject去干活儿不好吗

答案是,可以,但这样做会极大地增加渲染带来的性能损耗。

因为Widget具有不可变性但Element却是可变的。实际上Element树这一层将Widget树的变化类似React 虚拟DOM diff做了抽象可以只将真正需要修改的部分同步到真实的RenderObject树中最大程度降低对真实渲染视图的修改提高渲染效率而不是销毁整个渲染视图树重建。

就是Element树存在的意义。

RenderObject

从其名字我们就可以很直观地知道RenderObject是主要负责实现视图渲染的对象。

在前面的第4篇文章“Flutter区别于其他方案的关键技术是什么”中我们提到Flutter通过控件树Widget树中的每个控件Widget创建不同类型的渲染对象组成渲染对象树。

而渲染对象树在Flutter的展示过程分为四个阶段即布局、绘制、合成和渲染。 其中布局和绘制在RenderObject中完成Flutter采用深度优先机制遍历渲染对象树确定树中各个对象的位置和尺寸并把它们绘制到不同的图层上。绘制完毕后合成和渲染的工作则交给Skia搞定。

Flutter通过引入Widget、Element与RenderObject这三个概念把原本从视图数据到视图渲染的复杂构建过程拆分得更简单、直接在易于集中治理的同时保证了较高的渲染效率。

RenderObjectWidget介绍

通过第5篇文章“从标准模板入手体会Flutter代码是如何运行在原生系统上的”的介绍你应该已经知道如何使用StatelessWidget和StatefulWidget了。

不过StatelessWidget和StatefulWidget只是用来组装控件的容器并不负责组件最后的布局和绘制。在Flutter中布局和绘制工作实际上是在Widget的另一个子类RenderObjectWidget内完成的。

所以在今天这篇文章的最后我们再来看一下RenderObjectWidget的源码来看看如何使用Element和RenderObject完成图形渲染工作。

abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
  ...
}

RenderObjectWidget是一个抽象类。我们通过源码可以看到这个类中同时拥有创建Element、RenderObject以及更新RenderObject的方法。

但实际上,RenderObjectWidget本身并不负责这些对象的创建与更新

对于Element的创建Flutter会在遍历Widget树时调用createElement去同步Widget自身配置从而生成对应节点的Element对象。而对于RenderObject的创建与更新其实是在RenderObjectElement类中完成的。

abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
   
  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  ...
}

在Element创建完毕后Flutter会调用Element的mount方法。在这个方法里会完成与之关联的RenderObject对象的创建以及与渲染树的插入工作插入到渲染树后的Element就可以显示到屏幕中了。

如果Widget的配置数据发生了改变那么持有该Widget的Element节点也会被标记为dirty。在下一个周期的绘制时Flutter就会触发Element树的更新并使用最新的Widget数据更新自身以及关联的RenderObject对象接下来便会进入Layout和Paint的流程。而真正的绘制和布局过程则完全交由RenderObject完成

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  ...
  void layout(Constraints constraints, { bool parentUsesSize = false }) {...}
  
  void paint(PaintingContext context, Offset offset) { }
}

布局和绘制完成后接下来的事情就交给Skia了。在VSync信号同步时直接从渲染树合成Bitmap然后提交给GPU。这部分内容我已经在之前的“Flutter区别于其他方案的关键技术是什么?”中与你介绍过了,这里就不再赘述了。

接下来我以下面的界面示例为例与你说明Widget、Element与RenderObject在渲染过程中的关系。在下面的例子中一个Row容器放置了4个子Widget左边是Image而右边则是一个Column容器下排布的两个Text。

图3 界面示例

那么在Flutter遍历完Widget树创建了各个子Widget对应的Element的同时也创建了与之关联的、负责实际布局和绘制的RenderObject。

图4 示例界面生成的“三棵树”

总结

好了今天关于Widget的设计思路和基本原理的介绍我们就先进行到这里。接下来我们一起回顾下今天的主要内容吧。

首先我与你介绍了Widget渲染过程学习了在Flutter中视图数据的组织和渲染抽象的三个核心概念即Widget、 Element和RenderObject。

其中Widget是Flutter世界里对视图的一种结构化描述里面存储的是有关视图渲染的配置信息Element则是Widget的一个实例化对象将Widget树的变化做了抽象能够做到只将真正需要修改的部分同步到真实的Render Object树中最大程度地优化了从结构化的配置信息到完成最终渲染的过程而RenderObject则负责实现视图的最终呈现通过布局、绘制完成界面的展示。

最后在对Flutter Widget渲染过程有了一定认识后我带你阅读了RenderObjectWidget的代码理解Widget、Element与RenderObject这三个对象之间是如何互相配合实现图形渲染工作的。

熟悉了Widget、Element与RenderObject这三个概念相信你已经对组件的渲染过程有了一个清晰而完整的认识。这样我们后续再学习常用的组件和布局时就能够从不同的视角去思考框架设计的合理性了。

不过在日常开发学习中绝大多数情况下我们只需要了解各种Widget特性及使用方法而无需关心Element及RenderObject。因为Flutter已经帮我们做了大量优化工作因此我们只需要在上层代码完成各类Widget的组装配置其他的事情完全交给Flutter就可以了。

思考题

你是如何理解Widget、Element和RenderObject这三个概念的它们之间是一一对应的吗你能否在Android/iOS/Web中找到对应的概念呢

欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。