Flutter 的核心设计思想便是“一切皆 Widget”
Flutter 将视图树的概念进行了扩展,把视图数据的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。Widget 是 Flutter 世界里对视图的一种结构化描述,里面存储的是有关视图渲染的配置信息;Element 则是 Widget 的一个实例化对象,将 Widget 树的变化做了抽象,能够做到只将真正需要修改的部分同步到真实的 Render Object 树中,最大程度地优化了从结构化的配置信息到完成最终渲染的过程;而 RenderObject,则负责实现视图的最终呈现,通过布局、绘制完成界面的展示。
Widget
是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。
Flutter 将 Widget 设计成不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新,以数据驱动 UI 构建的方式简单高效。
这样做的缺点是,因为涉及到大量对象的销毁和重建,所以会对垃圾回收造成压力。不过,Widget 本身并不涉及实际渲染位图,所以它只是一份轻量级的数据结构,重建的成本很低。
由于 Widget 的不可变性,可以以较低成本进行渲染节点复用,因此在一个真实的渲染树中可能存在不同的 Widget 对应同一个渲染节点的情况,这无疑又降低了重建 UI 的成本。
Element
Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
Flutter 渲染过程,可以分为三步:
通过 Widget 树生成对应的 Element 树;
创建相应的 RenderObject 并关联到 Element.renderObject 属性上
构建成 RenderObject 树,以完成最终的渲染
而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject。如果跨过Element直接由Widget树命令RenderObject渲染将有巨大的性能开销。而Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。
RenderObject
主要负责实现视图渲染的对象。渲染对象树在 Flutter 的展示过程分为四个阶段,即布局、绘制、合成和渲染。
布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。
RenderObjectWidget
StatelessWidget 和 StatefulWidget 只是用来组装控件的容器,并不负责组件最后的布局和绘制。在 Flutter 中,布局和绘制工作实际上是在 Widget 的另一个子类 RenderObjectWidget 内完成的。
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 的方法。
对于 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,就可以构建出任意功能、任意复杂度的界面。
UI 编程范式
Flutter 的视图开发是声明式的,其核心设计思想就是将视图和数据分离
除了设计好 Widget 布局方案之外,还需要提前维护一套文案数据集,并为需要变化的 Widget 绑定数据集中的数据,使 Widget 根据这个数据集完成渲染。
命令式编程强调精确控制过程细节;而声明式编程强调通过意图输出结果整体。对应到 Flutter 中,意图是绑定了组件状态的 State,结果则是重新渲染后的组件。在 Widget 的生命周期内,应用到 State 中的任何更改都将强制 Widget 重新构建。
当你所要构建的用户界面不随任何状态信息的变化而变化时,需要选择使用 StatelessWidget,反之则选用 StatefulWidget。前者一般用于静态内容的展示,而后者则用于存在交互反馈的内容呈现中。
什么场景下应该使用 StatelessWidget 呢?
一个简单的判断规则是:父 Widget 是否能通过初始化参数完全控制其 UI 展示效果?如果能,那么我们就可以使用 StatelessWidget 来设计构造函数接口了。
正确评估你的视图展示需求,避免无谓的 StatefulWidget 使用,是提高 Flutter 应用渲染性能最简单也是最直接的手段。
除了我们主动地通过 State 刷新 UI 之外,在一些特殊场景下,Widget 的 build 方法有可能会执行多次。
State 生命周期
State 的生命周期可以分为 3 个阶段:创建(插入视图树)、更新(在视图树中存在)、销毁(从视图树中移除)
State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染。
构造方法是 State 生命周期的起点,Flutter 会通过调用 StatefulWidget.createState() 来创建一个 State。我们可以通过构造方法,来接收父 Widget 传递的初始化 UI 配置数据。这些配置数据,决定了 Widget 最初的呈现效果。
initState,会在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作,比如为状态变量设定默认值。
didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。
build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。
Widget 的状态更新,主要由 3 个方法触发:setState、didchangeDependencies 与 didUpdateWidget。
setState:我们最熟悉的方法之一。当状态数据发生变化时,我们总是通过调用这个方法告诉 Flutter更新UI。
didChangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调这个方法,随后触发组件构建。哪些情况下 State 对象的依赖关系会发生变化呢?典型的场景是,系统语言 Locale 或应用主题改变时,系统会通知 State 执行 didChangeDependencies 回调方法。
didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数。
一旦这三个方法被调用,Flutter 随后就会销毁老 Widget,并调用 build 方法重建 Widget。
组件销毁相对比较简单。比如组件被移除,或是页面销毁的时候,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件。
当组件的可见状态发生变化时,deactivate 函数会被调用,这时 State 会被暂时从视图树中移除。值得注意的是,页面切换时,由于 State 对象在视图树中的位置发生了变化,需要先暂时移除后再重新添加,重新触发组件构建,因此这个函数也会被调用。
-
当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 函数。而一旦到这个阶段,组件就要被销毁了,所以我们可以在这里进行最终的资源释放、移除监听、清理环境,等等。
image-20211029162633985.png
APP生命周期
App 的生命周期,则定义了 App 从启动到退出的全过程。在 Flutter 中,我们可以利用WidgetsBindingObserver类,来实现对APP生命周期的监听。
abstract class WidgetsBindingObserver {
// 页面 pop
Future didPopRoute() => Future.value(false);
// 页面 push
Future didPushRoute(String route) => Future.value(false);
// 系统窗口相关改变回调,如旋转
void didChangeMetrics() { }
// 文本缩放系数变化
void didChangeTextScaleFactor() { }
// 系统亮度变化
void didChangePlatformBrightness() { }
// 本地化语言变化
void didChangeLocales(List locale) { }
//App 生命周期变化
void didChangeAppLifecycleState(AppLifecycleState state) { }
// 内存警告回调
void didHaveMemoryPressure() { }
//Accessibility 相关特性回调
void didChangeAccessibilityFeatures() {}
}
常见的屏幕旋转、屏幕亮度、语言变化、内存警告都可以通过这个实现进行回调。我们通过给 WidgetsBinding 的单例对象设置监听器,就可以监听对应的回调方法。
didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。
resumed:可见的,并能响应用户的输入。
inactive:处在不活动状态,无法处理用户响应。
paused:不可见并不能响应用户的输入,但是在后台继续活动中。
帧绘制回调:
WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制:
- 单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次。
WidgetsBinding.instance.addPostFrameCallback((_){
print(" 单次 Frame 绘制回调 ");// 只回调一次
});
- 实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。
WidgetsBinding.instance.addPersistentFrameCallback((_){
print(" 实时 Frame 绘制回调 ");// 每帧都回调
});