简单来讲,Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层,下面我们分别介绍:
1. 框架层
Flutter Framework,即框架层。这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:
底下两层(Foundation 和 Animation、Painting、Gestures)在 Google 的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是 Flutter Engine 暴露的底层UI库,提供动画、手势及绘制能力。
Rendering 层,即渲染层,这一层是一个抽象的布局层,它依赖于 Dart UI 层,渲染层会构建一棵由可渲染对象的组成的渲染树,当动态更新这些对象时,渲染树会找出变化的部分,然后更新渲染。渲染层可以说是Flutter 框架层中最核心的部分,它除了确定每个渲染对象的位置、大小之外还要进行坐标变换、绘制(调用底层 dart:ui )。
Widgets 层是 Flutter 提供的的一套基础组件库,在基础组件库之上,Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库,它们分别实现了 Material 和 iOS 设计规范。
Flutter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 camera 和 webview ,以及和平台无关的功能,例如 animations 。
2. 引擎层
Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时(Dart runtime)、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到引擎层,然后实现真正的绘制和显示。
3. 嵌入层
Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。
通常来说,开发者不需要感知到Engine和Embedder的存在(如果不需要调用平台的系统服务),Framework是开发者需要直接交互的,因而也在整个分层架构模型的最上层。
在 Flutter 中, widget
的功能是“描述一个UI元素的配置信息”,它就是说, Widget
其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget
接收的参数,比如对于 Text
来讲,文本的内容、对齐方式、文本样式都是它的配置信息。下面我们先来看一下 Widget
类的声明:
// 不可变的
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
@factory
Element createElement();
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
bool operator ==(Object other) => super == other;
int get hashCode => super.hashCode;
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}
@immutable
代表 Widget 是不可变的,这会限制 Widget
中定义的属性(即配置信息)必须是不可变的(final
),为什么不允许 Widget
中定义的属性变化呢?这是因为,Flutter 中 如果属性发生变化则会重新构建Widget
树,即重新创建新的 Widget
实例来替换旧的 Widget
实例,所以允许 Widget
的属性变化是没有意义的,因为一旦 Widget
自己的属性变了自己就会被替换。这也是为什么 Widget
中定义的属性必须是 final
的原因。Widget
类继承自DiagnosticableTree
,DiagnosticableTree
即 “诊断树”,主要作用是提供调试信息。Key
: 这个key
属性类似于 React/Vue
中的key
,主要的作用是决定是否在下一次build
时复用旧的 widget
,决定的条件在canUpdate()
方法中。createElement()
:正如前文所述“一个 widget
可以对应多个Element
”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element
对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。debugFillProperties(...)
复写父类的方法,主要是设置诊断树的一些特性。canUpdate(...)
是一个静态方法,它主要用于 在 widget 树重新build时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidget
与oldWidget
的runtimeType
和key
同时相等时就会用new widget去更新Element
对象的配置,否则就会创建新的Element
。在Flutter中,可以说万事万物皆Widget
。哪怕一个居中的能力,在传统的命令式UI开发中,通常会以一个属性的方式进行设置,而在Flutter中则抽象成一个名为Center
的组件。此外,Flutter提倡激进式的组合开发,即尽可能通过一系列基础的Widget
构造出你的目标Widget
。
基于以上两个特点,Flutter的代码中将充斥着各种Widget
,每一帧UI的更新都意味着部分Widget
的重建。你可能会担心,这种设计是否过于臃肿和低效,其实恰恰相反,这正是Flutter能够进行高性能渲染的基石。Flutter之所以要如此设计也是基于以下两点事实有意而为之:
Widget Tree上的Widget
节点越多,通过Diff算法得到的需要重建的部分就越精确、范围越小,而UI渲染的主要性能瓶颈就是Widget
节点的重建。
Dart语言的对象模型 和 GC模型 对小对象的快读分配和回收做了优化,而Widget
正是这种小对象。
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?
Flutter 框架的的处理流程是这样的:
Widget
树生成一个 Element
树,Element
树中的节点都继承自 Element
类。Element
树生成 Render
树(渲染树),渲染树中的节点都继承自RenderObject
类。Layer
树,然后上屏显示,Layer
树中的节点都继承自 Layer
类。真正的布局和渲染逻辑在 Render
树中,Element
是 Widget
和 RenderObject
的粘合剂,可以理解为一个中间代理。
其中三棵树的各自的作用是:
Widget
:负责配置,为Element
描述UI的配置信息,拥有公开的Api,这部分也是开发者可以直接感知和使用的部分。Element
:Flutter Virtual DOM 的管理者,它管理widget
的生命周期,代表了内存中真实存在的UI数据,它负责树中特定位置的widget
的实例化,持有widget
的引用,管理树中的父子关系,实际上Widget Tree
和 RenderObject Tree
都是由Element Tree
驱动生成的。RenderObject
:负责处理大小、布局和绘制工作,它将绘制自身,摆放子节点等。这里需要注意:
Element
和 Widget
是 一 一 对应的,但 Element
和 RenderObject
不是 一 一 对应的。比如 StatelessWidget
和 StatefulWidget
都没有对应的 RenderObject
。Layer
树,因此在Flutter中实际上是有四棵树,但我们只需了解以上三棵树就行。StatelessWidget
相对比较简单,它继承自widget
类,重写了createElement()
方法:
createElement() => StatelessElement(this);
StatelessElement
StatelessElement
间接继承自Element
类,与StatelessWidget
相对应(作为其配置数据)。
StatelessWidget
用于不需要维护状态的场景,它通常在build
方法中通过嵌套其他 widget
来构建UI,在构建过程中会递归的构建其嵌套的 widget
。
下面是一个简单的例子:
class Echo extends StatelessWidget {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //默认为灰色
}):super(key:key);
final String text;
final Color backgroundColor;
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
然后我们可以通过如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
按照惯例,
widget
的构造函数参数应使用命名参数,命名参数中的必需要传的参数要添加required
关键字,这样有利于静态代码分析器进行检查;在继承widget
时,第一个参数通常应该是Key
。另外,如果widget
需要接收子widget
,那么child
或children
参数通常应被放在参数列表的最后。同样是按照惯例,widget
的属性应尽可能的被声明为final
,防止被意外改变。
build
方法有一个context
参数,它是BuildContext
类的一个实例,表示当前 widget
在 widget
树中的上下文,每一个 widget
都会对应一个 context
对象。
实际上,context
是当前 widget
在 widget
树中位置中执行”相关操作“的一个句柄(handle
), 比如它提供了从当前 widget
开始向上遍历 widget
树以及按照 widget
类型查找父级 widget
的方法。
下面是在子树中获取父级 widget
的一个示例:
class ContextRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context测试"),
),
body: Container(
child: Builder(builder: (context) {
// 在 widget 树中向上查找最近的父级`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
有了StatelessWidget
,为什么还要有一个StatefulWidget
呢?在Flutter中Widget
都是不可变的, 但实际上需要根据对应的状态刷新Widget
,于是就产生了StatefulWdiget
, StatefulWidget
是由2个对象Widget
和State
组成的。
StatefulWidget
同样是继承自widget
类,并重写了createElement()
方法,不同的是返回的Element
对象并不相同,返回的是一个StatefulElement
;另外StatefulWidget
类中添加了一个新的接口createState()
。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
StatefulElement createElement() => StatefulElement(this);
State createState();
}
StatefulElement
间接继承自Element
类,与StatefulWidget
相对应(作为其配置数据)。StatefulElement
中可能会多次调用createState()
来创建状态(State
)对象。开发者无需关心该方法。
createState()
用于创建和 StatefulWidget
相关的状态,开发者必须在子类中实现该方法。它在StatefulWidget
的生命周期中可能会被多次调用。例如,当一个 StatefulWidget
同时插入到 widget
树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State
实例,其实,本质上就是一个StatefulElement
对应一个State
实例。
而在
StatefulWidget
中,State
对象和StatefulElement
具有一 一对应的关系,所以在Flutter的SDK文档中,可以经常看到“从树中移除 State 对象”或“插入 State 对象到树中”这样的描述,此时的树指通过 widget 树生成的 Element 树。Flutter 的 SDK 文档中经常会提到“树” ,我们可以根据语境来判断到底指的是哪棵树。其实,对于用户来说我们无需关心到底是哪棵树,无论哪种树其最终目标都是为了描述 UI 的结构和绘制信息,我们只需理解为 “一棵构成用户界面的节点树”即可,不必过于纠结于这些概念。
一个 StatefulWidget
类会对应一个 State
类,State
表示与其对应的 StatefulWidget
要维护的状态,State
中的保存的状态信息可以:
State
在 widget
构建时可以被同步读取。State
在 widget
生命周期中可以被改变,可以手动调用setState()
方法改变State
,这会通知 Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build
方法重新构建 widget
树,从而达到更新UI的目的。State
中有两个常用属性:
State.widget
,通过它我们可以拿到与该 State
实例关联的 widget
实例,由 Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的 widget
实例在重新构建时可能会变化,但State
实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget
被修改了,Flutter 框架会动态设置 State.widget
指向新的 widget
实例。
State.context
,它是StatefulWidget
对应的 BuildContext
,作用同 StatelessWidget
的 BuildContext
。当框架需要创建一个StatefulWidget
时,它会调用StatefulWidget.createState()
方法,然后会将State
对象和 BuildContext
关联在一起,这种关联关系是永久的,State
对象永远不会改变它的BuildContext
,但是BuildContext
本身可以在树的周围移动。
在State
对象被createState()
方法创建之后,紧接着会调用initState()
方法并开始自己的生命周期:
其中各个回调函数的含义:
initState()
:当 widget
第一次插入到 widget
树时会被调用,对于每一个State
对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。
注意:不能在initState
中调用BuildContext.dependOnInheritedWidgetOfExactType
(该方法用于在 widget
树上获取离当前 widget
最近的一个父级InheritedWidget
),原因是在初始化完成后, widget
树中的InheritFrom widget
也可能会发生变化,所以正确的做法应该在在build()
方法或didChangeDependencies()
中调用它。
didChangeDependencies()
:当State
对象的依赖发生变化时会被调用;例如:在之前 build()
中包含了一个InheritedWidget
,然后在之后的 build()
中 Inherited widget
发生了变化,那么此时InheritedWidget
的子 widget
的didChangeDependencies()
回调都会被调用。典型的场景是当系统语言 Locale
或应用主题改变时,Flutter 框架会通知 widget
调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重新创建)对应的didChangeDependencies
也会被调用。
build()
:主要用于构建 widget
子树的,会在如下场景被调用:
initState()
之后。didUpdateWidget()
之后。setState()
之后。didChangeDependencies()
之后。State
对象从树中一个位置移除后(会调用deactivate
)又重新插入到树的其他位置之后。reassemble()
: 专门为了开发调试而提供的回调,在 热重载(hot reload) 时会被调用,此回调在Release模式下永远不会被调用。
didUpdateWidget()
:在 widget
重新构建时,Flutter 框架会调用widget.canUpdate
来检测 widget
树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate
返回true
则会调用此回调。
正如之前所述,widget.canUpdate
会在新旧 widget
的 key
和 runtimeType
同时相等时会返回true
,也就是说在这种情况时didUpdateWidget()
就会被调用。(实际上这里flutter框架会创建一个新的Widget,绑定本State,并在这个函数中传递老的Widget)上级节点rebuild widget时, 即上级组件状态发生变化时也会触发子widget执行didUpdateWidget
。需要注意的是,如果涉及到controller的变更,需要在这个函数中移除老的controller的监听,并创建新controller的监听。
deactivate()
:当 State
对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State
对象重新插到树中,如包含此 State
对象的子树在树的一个位置移动到另一个位置时(可以通过 GlobalKey
来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()
方法。
dispose()
:当 State
对象从树中被永久移除时调用;通常在此回调中释放资源、取消订阅、取消动画等。
通过以下计数器的例子来说明:
class CounterWidget extends StatefulWidget {
const CounterWidget({Key? key, this.initValue = 0});
final int initValue;
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void initState() {
super.initState();
//初始化状态
_counter = widget.initValue;
print("initState");
}
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: TextButton(
child: Text('$_counter'),
//点击后计数器自增
onPressed: () => setState(
() => ++_counter,
),
),
),
);
}
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget ");
}
void deactivate() {
super.deactivate();
print("deactivate");
}
void dispose() {
super.dispose();
print("dispose");
}
void reassemble() {
super.reassemble();
print("reassemble");
}
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
注意:在继承
StatefulWidget
重写其方法时,对于包含@mustCallSuper
标注的父类方法,都要在子类方法中调用父类方法。
接下来创建一个新路由页面,在其中,我们只显示一个CounterWidget
:
class StateLifecycleTest extends StatelessWidget {
const StateLifecycleTest({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return CounterWidget();
}
}
我们运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字0,然后控制台日志输出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在StatefulWidget
插入到 widget
树时首先initState
方法会被调用。
然后我们点击⚡️按钮热重载,控制台输出日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
可以看到在build
方法之前,只有 reassemble
和 didUpdateWidget
被调用。
接下来,我们在 widget
树中移除 CounterWidget
,将 StateLifecycleTest
的 build
方法改为:
Widget build(BuildContext context) {
//移除计数器
//return CounterWidget ();
//随便返回一个Text()
return Text("xxx");
}
然后热重载,日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
可以看到,在 CounterWidget
从 widget
树中移除时,deactive
和dispose
会依次被调用。
setState()
方法是Flutter中修改状态的唯一合法途径,该方法会通知框架此对象的内部状态已更改,调用方式为:setState(() {_myState = newValue;});
它会提供一个回调方法,开发者必须在该回调中修改状态值,该回调将立即被同步调用。注意决不能在该回调中执行耗时计算,更不能在其中返回一个Future
(回调不能是“async
”标记的),该回调通常只用于包装对状态的实际更改。
调用 setState()
后,会将当前widget
对应的element
对象标记为dirty
,随后会调用build()
方法刷新UI。下面是加入 setState()
后State的生命周期:
这样看来其实可以将State生命周期分为三个阶段:
initState()
、didChangeDependencies()
didUpdateWidget()
导致的build()
执行、由setState()
导致的build()
执行deactivate()
、dispose()
StatefulWidget
为什么要将 build
方法放在 State
类中,而不是放在 StatefulWidget
中?
如果将build()
方法放在StatefulWidget
中,从灵活性上考虑会有两个问题:
1. 状态访问不便。
试想一下,如果我们的StatefulWidget
有很多状态,而每次状态改变都要调用build
方法,由于状态是保存在 State
中的,如果build
方法在StatefulWidget
中,那么build
方法和 状态
分别在两个类中,那么构建时读取状态将会很不方便!试想一下,如果真的将build
方法放在 StatefulWidget
中的话,由于构建用户界面过程需要依赖 State
,所以build
方法将必须加一个State
参数,大概是下面这样:
Widget build(BuildContext context, State state){
//state.counter
...
}
这样的话就只能将State
的所有状态声明为公开的状态,这样才能在State
类外部访问状态!但是,将状态设置为公开后,状态将不再具有私密性,这就会导致对状态的修改将会变的不可控。但如果将build()
方法放在State
中的话,构建过程不仅可以直接访问状态,而且也无需公开私有状态,这会非常方便。
2. 继承 StatefulWidget 不便。
例如,Flutter 中有一个动画 widget 的基类AnimatedWidget
,它继承自StatefulWidget
类。AnimatedWidget
中引入了一个抽象方法build(BuildContext context)
,继承自AnimatedWidget
的动画 widget
都要实现这个build
方法。现在设想一下,如果StatefulWidget
类中已经有了一个build
方法,正如上面所述,此时build
方法需要接收一个 State
对象,这就意味着AnimatedWidget
必须将自己的 State
对象(记为_animatedWidgetState
)提供给其子类,因为子类需要在其build
方法中调用父类的build
方法,代码可能如下:
class MyAnimationWidget extends AnimatedWidget {
Widget build(BuildContext context, State state){
// 由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
// 所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState 暴露给其子类
super.build(context, _animatedWidgetState)
}
}
这样很显然是不合理的,因为AnimatedWidget
的状态对象是AnimatedWidget
内部实现细节,不应该暴露给外部。
如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。
综上所述,可以发现,对于StatefulWidget
,将build
方法放在 State
中,可以给开发带来很大的灵活性。
另一个方面,主要是性能考虑,State管理状态(可以理解为Controller),Widget是UI(即View),根据状态变化每次生成Widget(即View)可以节省内存,而不必每次创建状态对象State。
由于 StatefulWidget 的的具体逻辑都在其 State 中,所以很多时候,我们需要获取 StatefulWidget 对应的State 对象来调用一些方法,比如Scaffold组件对应的状态类ScaffoldState中就定义了打开 SnackBar(路由页底部提示条)的方法。我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。
context
对象有一个findAncestorStateOfType()
方法,该方法可以从当前节点沿着 widget
树向上查找指定类型的 StatefulWidget
对应的 State
对象。下面是实现打开 SnackBar
的示例:
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("子树中获取State对象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单1'),
);
}),
],
),
),
drawer: Drawer(),
);
}
}
一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of方法。这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单2'),
);
}),
又比如我们想显示 snackbar 的话可以通过下面代码调用:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("我是SnackBar")),
);
},
child: Text('显示SnackBar'),
);
}),
Flutter还有一种通用的获取State对象的方法——通过GlobalKey
来获取! 步骤分两步:
StatefulWidget
添加GlobalKey
。//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
GlobalKey
来获取State
对象_globalKey.currentState.openDrawer()
GlobalKey
是 Flutter 提供的一种在整个 App 中引用 element
的机制。如果一个 widget
设置了GlobalKey
,那么我们便可以通过globalKey.currentWidget
获得该 widget
对象、globalKey.currentElement
来获得 widget
对应的element
对象,如果当前 widget
是StatefulWidget
,则可以通过globalKey.currentState
来获得该 widget
对应的state
对象。
注意:使用
GlobalKey
开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个GlobalKey
在整个widget
树中必须是唯一的,不能重复。
可以看到,BuildOwner
通过StatefulElement
间接驱动了State
各个生命周期回调的触发。下面将从源码的角度分析每个回调的触发路径及其含义。
// 代码清单8-1 flutter/packages/flutter/lib/src/widgets/framework.dart
StatefulElement(StatefulWidget widget)
: state = widget.createState(), // 触发State创建
super(widget) {
state._element = this; // State持有Element和Widget的引用
state._widget = widget;
assert(state._debugLifecycleState == _StateLifecycle.created);
}
StatefulElement
在自身的构造函数中完成State
的创建,并完成了两个关键字段的赋值。此后会进行该Element
节点的Build
流程,具体逻辑如代码清单8-2所示。
// 代码清单8-2 flutter/packages/flutter/lib/src/widgets/framework.dart
void _firstBuild() {
try { // 触发initState回调
final dynamic debugCheckForReturnedFuture = state.initState() as dynamic;
} finally { ...... }
state.didChangeDependencies(); // 触发didChangeDependencies回调
super._firstBuild();
}
因为以上逻辑触发了生命周期中的initState
和didChangeDependencies
方法,所以 initState
一般会作为State
内部成员变量开始初始化的时间点。
didChangeDependencies
虽然在首次构建时也会无条件触发,但是它在后续Build流程中依然会被触发,如代码清单8-3所示。
// 代码清单8-3 flutter/packages/flutter/lib/src/widgets/framework.dart
// StatefulElement
void performRebuild() {
if (_didChangeDependencies) { // 通常在代码清单8-13中设置为true,详见8.2节
state.didChangeDependencies(); // 当该字段为true时再次触发didChangeDependencies
_didChangeDependencies = false;
}
super.performRebuild();
}
didChangeDependencies
标志该Element
的依赖节点发生了改变,此时 didChangeDependencies
方法会被再次调用,所以该回调比较适合响应一些依赖的更新。performnRebuild
最终还会触发StatefulElement
的build
方法,如代码清单8-4所示。
// 代码清单8-4 flutter/packages/flutter/lib/src/widgets/framework.dart
Widget build() => state.build(this);
以上逻辑符合使用StatefulWidget
的直觉,即build
方法是放在State
中的,该回调主要用于UI的更新。需要注意的是,如果当前Element
节点标记为dirty
,则build
方法一定会被调用,所以不宜进行耗时操作,以免影响UI的流畅度。
对于非首次Build的情况,通常会触发Element
的update
方法,对StatefulElement
来说,其逻辑如代码清单8-5所示。
// 代码清单8-5 flutter/packages/flutter/lib/src/widgets/framework.dart
void update(StatefulWidget newWidget) { // StatefulElement,见代码清单5-47
super.update(newWidget);
assert(widget == newWidget);
final StatefulWidget oldWidget = state._widget!;
_dirty = true;
state._widget = widget as StatefulWidget;
try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
final dynamic debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget)
as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild(); // 触发代码清单8-4中的逻辑
}
以上逻辑在通过rebuild
触发State
的build
方法之前,会触发其didUpdateWidget
方 法。移除对oldWidget
的一些引用和依赖,以及更新一些依赖Widget
属性的资源,通过该方法进行操作是一个合适的时机。
Element
节点在detach
阶段会调用_deactivateRecursively
方 法,其具体逻辑如代码清单8-6所示。
// 代码清单8-6 flutter/packages/flutter/lib/src/widgets/framework.dart
static void _deactivateRecursively(Element element) { // 见代码清单5-50
element.deactivate();
assert(element._lifecycleState == _ElementLifecycle.inactive);
element.visitChildren(_deactivateRecursively);
}
void deactivate() { // StatefulElement
state.deactivate(); // 触发deactivate回调
super.deactivate(); // 见代码清单8-7
}
以上逻辑会触发State
的deactivate
方法,并导致当前节点进入inactive
阶段。该回调触发说明当前Element
节点被移出Element Tree
,但仍有可用被再次加入,该时机适合释放一些和当前状态强相关的资源,而对于那些和状态无关的资源,考虑到该Element
节点仍有可能进入Element Tree
,并不适合在此时释放(可以类比Android中 Activity
的onPause
回调)。
而以上逻辑中super
的deactivate
方法是必须调用的,其逻辑如代码清单8-7所示。
// 代码清单8-7 flutter/packages/flutter/lib/src/widgets/framework.dart
void deactivate() { // Element
if (_dependencies != null && _dependencies!.isNotEmpty) { // 依赖清理
for (final InheritedElement dependency in _dependencies!)
dependency._dependents.remove(this);
}
_inheritedWidgets = null;
_lifecycleState = _ElementLifecycle.inactive; // 更新状态
}
以上逻辑主要用于移除对应的依赖。
在Build流程结束后,Element
的unmount
方法将被调用,其逻辑如代码清单8-8所示。
// 代码清单8-8 flutter/packages/flutter/lib/src/widgets/framework.dart
// StatefulElement,见代码清单5-52
void unmount() {
super.unmount();
state.dispose(); // 触发dispose回调
state._element = null;
}
void unmount() { // Element
final Key? key = _widget.key;
if (key is GlobalKey) {
key._unregister(this); // 取消注册
}
_lifecycleState = _ElementLifecycle.defunct;
}
以上逻辑中,StatefulElement
中主要会调用State
的dispose
方法。Element
中的通用逻辑负责销毁GlobalKey
相关的注册。
需要指出的是如果想要知道App的生命周期,那么需要通过WidgetsBindingObserver
的didChangeAppLifecycleState
来获取。通过该接口可以获取的生命周期状态在AppLifecycleState
类中。常用状态包含如下几个:
onResume
drawFrame
回调;同Android的onPause
drawFrame
回调(engine不会回调PlatformDispatcher
的onBeginFrame
和onDrawFrame
);同Android的onStop
drawFrame
回调;当应用处于此状态时,engine在没有视图的情况下运行。这可能是处在engine首次初始化时附加视图的过程,也可能是在由于Navigator
弹出而销毁视图之后。使用示例:
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
void didChangeAppLifecycleState(AppLifecycleState state) {
print(state);
}
}
时至今日,业界已经有很多跨平台框架(特指 Android 和 iOS 两个平台),根据其原理,主要分为三类:
上表中开发语言主要指应用层的开发语言,而开发效率,是指整个开发周期的效率,包括编码时间、调试时间、以及排错、处理兼容性问题时间。动态化主要指是否支持动态下发代码和是否支持热更新。值得注意的是 Flutter 的Release 包默认是使用 Dart AOT 模式编译的,所以不支持动态化,但 Dart 还有 JIT 或 snapshot 运行方式,这些模式都是支持动态化的。
这类框架主要原理就是将 App 中需要动态变动的内容通过HTML5(简称 H5)来实现,通过原生的网页加载控件WebView (Android)或 WKWebView(iOS)来加载。这种方案中,H5 部分是可以随时改变而不用发版,动态化需求能满足;同时,由于 H5 代码只需要一次开发,就能同时在 Android 和 iOS 两个平台运行,这也可以减小开发成本,也就是说,H5 部分功能越多,开发成本就越小。我们称这种 H5 + 原生 的开发模式为混合开发 ,采用混合模式开发的App我们称之为混合应用或 HTMLybrid App ,如果一个应用的大多数功能都是 H5 实现的话,我们称其为 Web App 。
目前混合开发框架的典型代表有:Cordova、Ionic 。大多数 App 中都会有一些功能是 H5 开发的,至少目前为止,HTMLybrid App 仍然是最通用且最成熟的跨端解决方案。
在此,我们需要提一下小程序,目前国内各家公司小程序应用层的开发技术栈是 Web 技术栈,而底层渲染方式基本都是 WebView 和原生相结合的方式。
1) 示例:JavaScript调用原生API获取手机型号
下面我们以 Android 为例,实现一个获取手机型号的原生 API 供 JavaScript
调用。在这个示例中将展示 JavaScript
调用原生 API 的流程。我们选用 Github上开源库的 dsBridge 作为 JsBridge
来进行通信。
class JSAPI {
public Object getPhoneModel(Object msg) {
return Build.MODEL;
}
}
WebView
注册到JsBridge
中import wendu.dsbridge.DWebView
...
//DWebView继承自WebView,由dsBridge提供
DWebView dwebView = (DWebView) findViewById(R.id.dwebview);
//注册原生API到JsBridge
dwebView.addJavascriptObject(new JsAPI(), null);
JavaScript
中调用原生APIvar dsBridge = require("dsbridge")
//直接调用原生API `getPhoneModel`
var model = dsBridge.call("getPhoneModel");
//打印机型
console.log(model);
上面示例演示了 JavaScript
调用原生 API 的过程,同样的,一般来说优秀的 JsBridge
也支持原生调用 JavaScript
,dsBridge
也是支持的,如果感兴趣,可以去 Github dsBridge 项目主页查看。
现在,我们回头来看一下,混合应用无非就是在第一步中预先实现一系列 API 供 JavaScript
调用,让 JavaScript
有访问系统功能的能力,看到这里相信你也可以自己实现一个混合开发框架了。
混合应用的优点是:动态内容可以用 H5开发,而H5是Web 技术栈,Web技术栈生态开放且社区资源丰富,整体开发效率高。缺点是性能体验不佳,对于复杂用户界面或动画,WebView 有时会不堪重任。
React 是一个响应式的 Web 框架,我们先了解一下两个重要的概念:DOM 树与响应式编程。
DOM 树
文档对象模型(Document Object Model,简称DOM),是 W3C 组织推荐的处理可扩展标志语言的标准编程接口,一种独立于平台和语言的方式访问和修改一个文档的内容和结构。换句话说,这是表示和处理一个 HTML 或XML 文档的标准接口。简单来说,DOM 就是文档树,与用户界面控件树对应,在前端开发中通常指 HTML 对应的渲染树,但广义的 DOM 也可以指 Android 中的 XML 布局文件对应的控件树,而术语DOM操作就是指直接来操作渲染树(或控件树), 因此,可以看到其实 DOM 树和控件树是等价的概念,只不过前者常用于 Web 开发中,而后者常用于原生开发中。
响应式编程
React 中提出一个重要思想:状态改变则UI随之自动改变。而 React 框架本身就是响应用户状态改变的事件而执行重新构建用户界面的工作,这就是典型的 响应式 编程范式,下面我们总结一下 React 中响应式原理:
值得注意的是,在第二步中,状态变化后 React 框架并不会立即去计算并渲染 DOM 树的变化部分,相反,React会在 DOM 树的基础上建立一个抽象层,即虚拟DOM树,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟 DOM ,最后再批量同步到真实 DOM 中,而不是每次改变都去操作一下DOM。
为什么不能每次改变都直接去操作 DOM 树?这是因为在浏览器中每一次 DOM 操作都有可能引起浏览器的重绘或回流(重新排版布局,确定 DOM 节点的大小和位置):
而浏览器的重绘和回流都是比较昂贵的操作,如果每一次改变都直接对 DOM 进行操作,这会带来性能问题,而批量操作只会触发一次 DOM 更新,会有更高的性能。
React Native (简称 RN )是 Facebook 于 2015 年 4 月开源的跨平台移动应用开发框架,是 Facebook 早先开源的 Web 框架 React 在原生移动应用平台的衍生产物,目前支持 iOS 和 Android 两个平台。RN 使用JSX 语言(扩展后的 JavaScript,主要是可以在 JavaScript 中写 HTML标签)和 CSS 来开发移动应用。因此,熟悉 Web 前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。
由于 RN 和 React 原理相通,并且 Flutter在应用层也是受 React 启发,很多思想也都是相通的。
上文已经提到 React Native 是 React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟 DOM 映射的对象是什么。React中虚拟 DOM 最终会映射为浏览器 DOM 树,而 RN 中虚拟 DOM会通过 JavaScriptCore 映射为原生控件。
JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:
为 JavaScript 提供运行环境。
是 JavaScript 与原生应用之间通信的桥梁,作用和 JsBridge 一样,事实上,在 iOS 中,很多 JsBridge 的实现都是基于 JavaScriptCore 。
而 RN 中将虚拟 DOM 映射为原生控件的过程主要分两步:
布局消息传递; 将虚拟 DOM 布局信息传递给原生;
原生根据布局信息通过对应的原生控件渲染;
至此,React Native 便实现了跨平台。 相对于混合应用,由于React Native是 原生控件渲染,所以性能会比混合应用中 H5 好一些,同时 React Native 提供了很多原生组件对应的 Web 组件,大多数情况下开发者只需要使用 Web 技术栈 就能开发出 App。我们可以发现,这样也就做到了维护一份代码,便可以跨平台了。
Weex 是阿里巴巴于 2016 年发布的跨平台移动端开发框架,思想及原理和 React Native 类似,底层都是通过原生渲染的,不同是应用层开发语法 (即 DSL,Domain Specific Language):Weex 支持 Vue 语法和 Rax 语法,Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造,而 RN 的 DSL 是基于 React 的,不支持 Vue。
JavaScript 开发 + 原生渲染 的方式主要优点如下:
不足:
这种技术的思路是:通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,所以可以做到不同平台UI的一致性。
注意,自绘引擎解决的是 UI 的跨平台问题,如果涉及其他系统能力调用,依然要涉及原生开发。这种平台技术的优点如下:
性能高;由于自绘引擎是直接调用系统API来绘制UI,所以性能和原生控件接近。
灵活、组件库易维护、UI外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到高保真和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。
不足:
也许你已经猜到 Flutter 就属于这一类跨平台技术,没错,Flutter 正是实现一套自绘引擎,并拥有一套自己的 UI 布局系统,且同时在开发效率上有了很大突破。不过,自绘制引擎的思路并不是什么新概念,Flutter并不是第一个尝试这么做的,在它之前有一个典型的代表,即大名鼎鼎的Qt。
Qt 是一个1991年由 Qt Company 开发的跨平台 C++ 图形用户界面应用程序开发框架。2008年,Qt Company 科技被诺基亚公司收购,Qt 也因此成为诺基亚旗下的编程语言工具。2012年,Qt 被 Digia 收购。2014年4月,跨平台集成开发环境 Qt Creator 3.1.0 正式发布,实现了对于 iOS 的完全支持,新增 WinRT、Beautifier 等插件,废弃了无 Python 接口的 GDB 调试支持,集成了基于 Clang 的 C/C++ 代码模块,并对 Android 支持做出了调整,至此实现了全面支持 iOS、Android、WP,它提供给应用程序开发者构建图形用户界面所需的所有功能。
但是,Qt 虽然在 PC 端获得了巨大成功,备受社区追捧,然而其在移动端却表现不佳,在近几年很少能听到 Qt 的声音,尽管 Qt 是移动端开发跨平台自绘引擎的先驱,但却成为了烈士。究其原因,可能有以下几点:
Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。Flutter 和 Qt mobile 一样,都没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。那么,我们会担心,Qt mobile 面对的问题Flutter是否也一样,Flutter会不会步入Qt mobile后尘,成为另一个烈士?Flutter从2017 年诞生至今,经历了多年时间的历练,Flutter 的生态系统得以快速增长,国内外有非常多基于 Flutter 的成功案例,国内的互联网公司基本都有专门的 Flutter 团队。总之,Flutter 发展飞快,已在业界得到了广泛的关注和认可,在开发者中受到了热烈的欢迎,成为了移动跨端开发中最受欢迎的框架之一。
现在,我们来和 Qt mobile做一个对比:
Flutter使用Dart作为开发语言和运行时机制,Dart一直保留着运行时机制,无论是在调试模式(debug)还是发布模式(release),但是两种构建方式之间存在很大的差异。
Dart的运行时包括一个非常重要的组件:垃圾回收器,它主要的作用就是在一个对象被实例化(instantiated)或者变成不可达(unreachable)时,分配和释放内存。
在Flutter运行过程中,会有很多的Object。在StatelessWidget
在渲染前(其实上还有StatefulWidget
),他们被创建出来。当状态发生变化的时候,他们又会被销毁。事实上,他们有很短的寿命。当我们构建一个复杂的UI界面时,会有成千上万这样的Widget
。
所以,作为Flutter开发者,我们是否需要担心垃圾回收器能否很好的帮助我们管理这些?(是否会带来很多的性能问题?)随着Flutter频繁地创建和销毁这些Widget
(Objects)对象,开发人员是否应该采取措施限制这种行为?
Widget
引用,并将它们放在State
中,这样它们就不会被破坏和重建,这并不罕见。这样做大可不必
Widget
即可。Dart的GC是分代的(generational)和由两个阶段构成:Young Space Scavenger(scavenger针对年轻一袋进行回收) 和 Parallel Marking and Concurrent Sweeping(sweep collectors针对老一代进行回收)
为了最大限度地减少GC对应用程序和UI性能的影响,垃圾收集器为Flutter引擎提供了hooks,当引擎检测到应用程序空闲且没有用户交互时,该hooks会提醒它。这使垃圾收集器窗口有机会在不影响性能的情况下运行其收集阶段。
垃圾收集器还可以在这些空闲间隔期间运行滑动压缩(sliding compaction),从而通过减少内存碎片来最大程度地减少内存开销。
这个阶段主要是清理一些寿命很短的对象,比如StatelessWidget
。当它处于阻塞时,它的清理速度远快于第二代的mark/sweep方式。并且结合调度,完成可以消除程序运行时的暂停现象。
本质上,对象被分配到内存中的一个连续空间,当对象被创建时,它们被分配到下一个可用空间,直到分配的内存被填满。Dart使用碰撞指针(bump pointer)分配来快速分配新的空间,使过程非常快速。(如果像malloc一样,维护free_list再分配,效率很低)
分配新对象的新空间由两半组成,称为半空间。任何时候都只使用一半空间:一半处于活动状态,另一半处于非活动状态。新对象被分配到活动的一半区域,一旦活动的一半被填充完毕,活动对象就会从活动区域copy到非活动区域,并且清除死亡的Object。然后,不活动的一半就变为了活动状态,并重复该过程。(这跟JVM的分代回收策略中新生代的幸存者空间很像)
为了确定哪些Object是存活的或死亡的,GC从根对象(如堆栈变量)开始检查它们引用的内容。然后将有引用的Object(存活的)移动到非活动状态,直接所有的存活Object被移动。死亡的Object没有参照物,因此被留下来;在将来的垃圾收集事件中,活动对象将被复制到它们上面。
有关此的更多信息,请查看Cheney算法。
当对象达到一定的寿命(在第一阶段没有被GC回收),它们会被提升到一个由第二代收集器管理的新的内存空间:mark-sweep。
这种垃圾收集技术有两个阶段:首先遍历对象图,然后标记仍在使用的对象。在第二阶段,扫描整个内存,并回收任何未标记的对象。然后清除所有标志。
这种GC技术阻塞了标记阶段;不会发生内存突变,并且UI线程也会被阻塞。但是由于短暂的对象在Young Space Scavenger阶段已经被处理,所以这种情况阶段非常罕见。但有时Dart运行时需要暂停才能运行这种形式的GC。考虑到Flutter计划收集的能力,应将其影响降至最低。
需要注意的是,如果一个应用程序不遵循分代的假设(即大多数对象英年早逝的假设),那么这种形式的GC将更频繁地发生。考虑到Flutter中Widget
的工作机制,这不太可能发生,但需要了解。
值得注意的是,Dart中的Isolate
机制具有私有堆的概念,彼此是独立的。每个Isolate
有自己单独的线程来运行,每个Isolate
的GC不影响其他Isolate
的性能。使用Isolate
是避免阻塞UI和卸载流程密集型活动的好方法。(耗时操作可以使用Isolate)
到这里你应该明白:Dart采用了一个强大的分代垃圾收集器,以最大限度的减少Flutter中GC带来的性能影响。 所以,你不需要担心Dart的垃圾回收器,可以放心的把精力放在业务上。
参考: