Flutter 的核心设计思想便是“一切皆 Widget”
Flutter 将视图树的概念进行了扩展,把视图数据的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。Widget 是 Flutter 世界里对视图的一种结构化描述,里面存储的是有关视图渲染的配置信息;Element 则是 Widget 的一个实例化对象,将 Widget 树的变化做了抽象,能够做到只将真正需要修改的部分同步到真实的 Render Object 树中,最大程度地优化了从结构化的配置信息到完成最终渲染的过程;而 RenderObject,则负责实现视图的最终呈现,通过布局、绘制完成界面的展示。
![](https://img.haomeiwen.com/i2659589/95a677983156af09.png)
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 生命周期
![](https://img.haomeiwen.com/i2659589/0277fbe88ed1adc1.png)
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<bool> didPopRoute() => Future<bool>.value(false);
// 页面 push
Future<bool> didPushRoute(String route) => Future<bool>.value(false);
// 系统窗口相关改变回调,如旋转
void didChangeMetrics() { }
// 文本缩放系数变化
void didChangeTextScaleFactor() { }
// 系统亮度变化
void didChangePlatformBrightness() { }
// 本地化语言变化
void didChangeLocales(List<Locale> locale) { }
//App 生命周期变化
void didChangeAppLifecycleState(AppLifecycleState state) { }
// 内存警告回调
void didHaveMemoryPressure() { }
//Accessibility 相关特性回调
void didChangeAccessibilityFeatures() {}
}
常见的屏幕旋转、屏幕亮度、语言变化、内存警告都可以通过这个实现进行回调。我们通过给 WidgetsBinding 的单例对象设置监听器,就可以监听对应的回调方法。
didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。
-
resumed:可见的,并能响应用户的输入。
-
inactive:处在不活动状态,无法处理用户响应。
-
paused:不可见并不能响应用户的输入,但是在后台继续活动中。
![](https://img.haomeiwen.com/i2659589/ad0a59a31ed8265d.png)
帧绘制回调:
WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制:
- 单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次。
WidgetsBinding.instance.addPostFrameCallback((_){
print(" 单次 Frame 绘制回调 ");// 只回调一次
});
- 实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。
WidgetsBinding.instance.addPersistentFrameCallback((_){
print(" 实时 Frame 绘制回调 ");// 每帧都回调
});
网友评论