Widgets
、Elements
、BuildContext
到底是什么东西?为什么 Flutter 可以运行那么快?为什么有时候运行的效果并不符合我们的预期?什么是所谓视图树?——本文将一一为你解答。Widgets
、Elements
、BuildContext
、RenderObject
这些都是些什么东西?Widgets
、Elements
、BuildContext
到底是什么东西?为什么 Flutter 可以运行那么快?为什么有时候运行的效果并不符合我们的预期?什么是所谓视图树?Widgets
,用它来展示 UI 并处理屏幕交互。但你是否考虑过,整个系统是如何工作的,是如何知道要更新哪些 UI 呢?-
屏幕事件 (点击屏幕 ) -
网络事件 (与服务器通信) -
时间事件 (动画) -
其它传感器事件
-
设备级别的属性更改 (设备方向改变,设置修改,内存问题,APP状态修改等) -
屏幕级别的更改(手势) -
平台渠道发送的数据 -
在 Flutter 引擎层空闲下来,可以渲染新的帧的时候,会发送通知给 Flutter 框架层。
-
手势 (屏幕上的事件) -
平台消息(如 GPS) -
硬件消息(如旋转屏幕, 应用压后台,内存不足等) -
异步消息( Future API 或者 HTTP 响应)
注: 一般情况下,如果Flutter渲染引擎没有发出通知, Flutter 框架是不能更新任何UI的。 有些时候,在没有 Flutter 渲染引擎通知的情况下,也可以让 Flutter 框架更新UI,但是并不建议这么做。
如果你想更新UI, 或者说你想在后台执行代码逻辑并更新 UI,你需要告诉 Flutter 引擎,这里有一些更改需要被渲染到屏幕上。通常情况下,在屏幕下一次刷新的时候, Flutter引擎会通知 Flutter框架,让它来提供新场景的图像来进行渲染显示。
-
像手势、http 网络请求和异步事件,它们都会触发一个异步任务,当它们引起 UI 的更新。它们会发送一个消息(Schedule Frame)给 Flutter引擎,告诉 Flutter引擎,有新的UI需要被渲染。 -
当 Flutter引擎准备好,可以更新UI的时候,它会发送 Begin Frame 通知到Flutter框架。 -
Flutter 框架运行着的异步任务,如动画,它们会拦截掉 Begin Frame 通知。 -
这些异步任务会根据自身状态进行判断是否需要继续发送请求给 Flutter 引擎,用来触发后续的UI渲染(例:当一个动画没有完成的时候,为了让动画可以继续执行,它会发送一个通知到 Flutter 引擎,然后会等待接收另一个 Begin Frame 的通知)。 -
紧接着,Flutter 引擎会发出一个 Draw Frame 的通知到 Flutter 框架层。 -
Flutter 框架会拦截 Draw Frame 通知,并根据任务进行布局调整和UI大小计算。 -
完成这些任务后,它将继续执行与更新布局有关的绘画任务。 -
如果有什么要画在屏幕上,它会发送一个全新的场景数据到 Flutter 引擎,让Flutter引擎来更新到屏幕上。 -
最后,Flutter 框架执行完所有的任务并且在屏幕中渲染完成。 -
紧接着会继续一遍又一遍的执行上述流程。
RenderObject
,它被用来表示:-
定义屏幕中的区域,包括 大小,位置, 几何结构。也可称其为"渲染内容"。 -
识别可能受到手势影响的屏幕区域。
RenderObject
共同组成了一棵树,称之为视图树。在视图树的最上面,也就是其跟节点,就是RenderView。RenderView
代表了整个输出的视图树,它也是一种特殊的 Renderobject
, 如图所示:Widgets
与 RenderObjects
之前的关系。但在这之前,我们需要更深入的了解 RenderObjects
。main()
方法,它会调用 runApp(Widget app)
。-
SchedulerBinding -
GestureBinding -
RendererBinding -
WidgetsBinding
-
ServicesBinding :处理不同平台发过来的消息 -
PaintingBinding:处理图片缓存 -
SemanticsBinding:保留到以后实现所有与语义相关的内容 -
TestWidgetsFlutterBinding:组件测试使用的
-
第一个是告诉 Flutter 引擎:“我现在已经准备好了,在你不忙的时候,把我唤醒,告诉我要渲染的内容,我会开始工作。” -
第二个是监听并响应一些事件,如唤醒事件。
当SchedulerBinding接受到唤醒事件 的时候,要做些什么呢?
-
需要 Ticker 来控制的时候
举个例子,假设您有一个动画,并且已经开始执行了。这个动画是由Ticker进行控制的,它需要以固定时间间隔触发回调。要让这样的回调运行,我们需要告诉Flutter 引擎在下次刷新时唤醒我们(发送Begin Frame),触发回调,执行动画任务。在该动画任务结束时,动画还需要继续,它将再次调用SchedulerBinding来调度另一帧。
-
更改布局
当你响应导致视觉变化的事件(例如,更新屏幕的一部分的颜色,滚动,向屏幕中添加/从屏幕中删除某些内容)时,我们需要采取必要的步骤来保证它可以正常的显示在屏幕上。在这种情况下,Flutter框架将调用SchedulerBinding来告诉Flutter Engine去调度另一帧。
GestureBinding
这个 binding 处理手势事件,并与 Flutter 引擎进行通信。它负责接受与手指有关的数据,并确定屏幕的哪一部分受到手势的影响。然后,它会通知这些部分,来响应事件。
RendererBinding
它是 Flutter 引擎与视图树之前的桥梁,它有两个不同的功能:
-
第一个是监听Flutter引擎发出的消息,当设备设置发生更改,它会告知用户受到影响的视觉效果/语义。 -
第二个是为 Flutter 引擎提供要显示在屏幕上的数据。
PipelineOwner是一种协调器,它知道哪个RenderObject需要做一些与布局有关的事情并协调这些动作。
-
第一个是负责处理Widgets结构变更的过程; -
第二个是触发渲染事件。
一些小组件的结构更改是 BuildOwner 来完成的,它跟踪需要重建的小部件,并处理应用于整个小部件结构的其他任务。
第二部分:Widgets 转换成像素
基础的内部原理讲解完成,下面我们来看看 Widgets。
在所有有关 Flutter 的文档中,你能看到这样子的描述:在Fluter中,所有的对象都是 Widgets 。这样子说虽然没错,但要说得更精确一些,我觉得应该是:
从开发人员的角度来看,在布局和交互方面,与用户界面相关的所有内容均通过Widgets完成。
1@immutable
2abstract class Widget extends DiagnosticableTree {
3 const Widget({ this.key });
4
5 final Key key;
6
7 ...
8}
1Widget build(BuildContext context){
2 return SafeArea(
3 child: Scaffold(
4 appBar: AppBar(
5 title: Text('My title'),
6 ),
7 body: Container(
8 child: Center(
9 child: Text('Centered Text'),
10 ),
11 ),
12 ),
13 );
14}
1Widget build(BuildContext context){
2 return MyOwnWidget();
3}
MyOwnWidget
会自己去渲染 SafeArea, Scaffold 等 Widget。这个例子,要表达的意思是:Widget 可能是一个页子节点,也可能是一颗树。
注:在这里我们使用了 "Widget 树",这只是为了更好的理解逻辑,在 Flutter中并没有这个概念。
每个小部件都对应一个元素。元素彼此链接并形成一棵树。因此,元素是树中某个节点的引用。
-
没有 Widgets 树,但是有元素树; -
Widgets 创建 Elements; -
Element 指向创建它的 Widget; -
Elements 使用父子关系进行关联; -
Elements 有一个或多个子节点; -
Elements 也可以指向一个 RenderObject。
Elements 定义了视图部分的链接关系。
-
代理类
InheritedWidget
和 LayoutId
这些 Widgets 不会展现出任何的用户页面,但是它们会用来为其它的Widgets提供数据。-
渲染类
-
大小尺寸
-
UI位置
-
布局/渲染方式
Row
,Column
,Stack
、Padding
、Align
、Opacity
、RawImage
等。-
组件类
RaisedButton
, Scaffold
, Text
, GestureDetector
, Container。
Widgets 分类
-
组件,此分类不直接用于任何视觉渲染的部分。 -
渲染,此分类是直接用于屏幕渲染。
在Flutter中,整个系统都依赖 Widget / RenderObject 的状态。
-
使用 setState 方法,这个方法可以用于所有的 StatefulElement (注意,我这里面说的不是 StatefulWidget)。 -
使用通知,基于 ProxyElement来实现状态更新(如:InheritedWidget)。
-
大小、位置等修改; -
需要重绘,如背景颜色修改、字体样式修改。
-
第1步,元素
WidgetsBinding被执行的时候, Flutter引擎首先考虑的是元素的变化。因为由建造者自己管理元素树,所以绑定控件的时候,会调用建造者的 buildScope 方法。这个方法中,会将要更改的元素存起来,稍后触发他们进行重建。
rebuild()
主要的原则如下:
-
大部分时候,触发元素的重建,会调用控件的
build()
方法(Widget build(BuildContext context) {….}),这个方法会返回一个新的控件。
-
如果元素没有子节点,这个元素就被创建完成,反之,会先创建子节点。
-
将新控件与元素引用的子控件进行比较:
-
如果可以被替换, 则更新,并保留子控件;
-
如果不可以被替换,子控件会被移除,并创建一个新的。
widget.createState()
方法,创建并关联对应的状态。RenderObjectElement 在元素被加载的时候,会创建一个 RenderObject, 并且会将这个对象加入到渲染树。-
第2步,渲染对象
drawFrame()
的整个调用流程:-
为每一个标记为脏的渲染对象计算新的布局(计算大小和几何形状);
-
使用渲染层,将所有需要重绘的对象重画出来;
-
将生成的场景数据发送给 Flutter 引擎,然后在屏幕中显示出来;
-
最后,Semantics 被发送更新到 Flutter 引擎。
window.onPointerDataPacket
方法发送出手势相关的事件, GestureBinding 会拦截并处理:-
Flutter 引擎将屏幕位置转换成对应的坐标;
-
拿到坐标上所有渲染出来的View对应的 RenderObject;
-
然后遍历所有的RenderObject ,并把对应事件分发给他们;
-
RenderObject 会等待它能处理的时间并处理它。
dart
abstract class Element extends DiagnosticableTree implements BuildContext {
...
}
除了一下的两种情况下,其他时候 BuildContext 是没有任何用处的:
控件被重建的时候; 在StatefulWidget链接到你引用的上下文变量的状态。
-
获得对应于控件的渲染对象的基准;
-
获取RenderObject的大小;
-
访问树——这是实际使用的所有小部件通常实施该方法的(例如MediaQuery.of(Context),Theme.of(Context))。
小例子
我们知道 BulidContext 也是一个元素,我给你展示一种有关 BuildContext 的使用方法。下面的代码可以使 StatelessWidget 更新,但是并不使用 setState 方法,而是使用 BuildContext:
1 void main(){
2 runApp(MaterialApp(home: TestPage(),));
3 }
4
5 class TestPage extends StatelessWidget {
6 // final because a Widget is immutable (remember?)
7 final bag = {"first": true};
8
9 @override
10 Widget build(BuildContext context){
11 return Scaffold(
12 appBar: AppBar(title: Text('Stateless ??')),
13 body: Container(
14 child: Center(
15 child: GestureDetector(
16 child: Container(
17 width: 50.0,
18 height: 50.0,
19 color: bag["first"] ? Colors.red : Colors.blue,
20 ),
21 onTap: (){
22 bag["first"] = !bag["first"];
23 //
24 // This is the trick
25 //
26 (context as Element).markNeedsBuild();
27 }
28 ),
29 ),
30 ),
31 );
32 }
33 }
与执行 setState 方法相同,其核心都是执行 _element.markNeedsBuild()
方法。
结语
我认为了解Flutter的架构是很有趣的,所有东西都被设计为高效,可扩展且对将来的扩展开放。而且,诸如Widget,Element,BuildContext,RenderObject之类的关键概念并不总是显而易见。
我希望本文对你有用。
热 文 推 荐
☞三年一跳槽、拒绝“唯学历”,火速 Get 这份程序员求生指南!
☞“国家队”入局! 中移动、银联等宣布区块链服务网络(BSN)正式内测!
点击阅读原文,参与中国开发者现状调查问卷!
文章评论