640?wx_fmt=gif
Flutter 内部是如何工作的? WidgetsElementsBuildContext 到底是什么东西?为什么 Flutter 可以运行那么快?为什么有时候运行的效果并不符合我们的预期?什么是所谓视图树?——本文将一一为你解答。
640?wx_fmt=jpeg
作者 | Didier Boelens
译者 | 罗昭成,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下为译文:
Flutter 内部是如何工作的? WidgetsElementsBuildContextRenderObject 这些都是些什么东西?
 
640?wx_fmt=png
简介
 
去年,我刚开始使用 Flutter 开发应用程序时,在互联网上,只能找到极少量Flutter相关文档。虽然有一些文章在谈论 Flutter,但基本没有文章写 Flutter 内部的工作原理。
WidgetsElementsBuildContext 到底是什么东西?为什么 Flutter 可以运行那么快?为什么有时候运行的效果并不符合我们的预期?什么是所谓视图树?
在Flutter开发一个应用程序时,有 95% 的需求,都只需要处理 Widgets ,用它来展示 UI 并处理屏幕交互。但你是否考虑过,整个系统是如何工作的,是如何知道要更新哪些 UI 呢?
 
640?wx_fmt=png
第一部分:背景
 
这一部份内容包含一些关键概念,了解他们,会有助于理解后面的内容。
硬件
让我们从最基础的东西开始说起。
当你在使用你的设备的时候,或者说你在使用某个应用程序的时候,你只会看到一块屏幕。
事实上,在屏幕上,你看到了一系列的像素点,这些像素点,共同组成了一个二维的图像。当你触摸屏幕的时候,屏幕也只识别你手指在屏幕中的位置。
神奇的是,我们的应用程序可以触发屏幕显示的图像更新。在以下情况下,应用程序可以触发屏幕显示图像更新:
  • 屏幕事件 (点击屏幕 )
  • 网络事件 (与服务器通信)
  • 时间事件 (动画)
  • 其它传感器事件
将图片渲染到屏幕上是由显示屏硬件来保证,这些显示屏都是按固定的间隔时间来刷新屏幕,通常是1秒钟刷新60次。这个刷新的频率,我们通常叫作 刷新率,用 HZ 表示。
设备从 GPU 接收到要在屏幕上显示的数据,渲染显示在屏幕上(注:GPU 是一种专用的电子电路,经过优化,可以从 polygons 和 textures 获取数据,并快速生成图像)。我们将 GPU 在每秒中生成并发送给设备用于显示的图像的次数叫做 帧率, 使用 FPS 来做为单位进行计量。
看到这里,你也许会问,在文章的开篇,我们写到二维图像与硬件屏幕的渲染,这些又和 Flutter 的 Widgets 有什么关系呢?
答案很简单,因为 Flutter 的一个核心功能就是合成二维图像并处理交互。我认为,从这个角度来解读,可以更好的理解 Flutter 的内部工作原理。
当然,还有一个原因,不管你信或者不信,Flutter 中几乎所有的事情都是由刷新屏幕的需求来驱动的,我们需要在适当的时候来快速刷新我们的屏幕。
接口设计
不管什么时候,只要你加入Flutter开发的行列中,都会看到下面这张 Flutter 的架构图。

640?wx_fmt=png

我们在开发 Flutter 的应用程序的时候,使用 Dart 语言,我们面象的 API 都是 Flutter框架(绿色部分) 这一层提供的。
Flutter 框架通过 Window 这个抽象层与 Flutter引擎(蓝色部分)进行通信,   Window 中抽象出来了一系列的 API,实现了与硬件通信的接口。
当然,在下面的这些情况中,Flutter引擎也可以通过 Window 来通知 Flutter 框架层来进行事件处理。
  • 设备级别的属性更改 (设备方向改变,设置修改,内存问题,APP状态修改等)
  • 屏幕级别的更改(手势)
  • 平台渠道发送的数据
  • 在 Flutter 引擎层空闲下来,可以渲染新的帧的时候,会发送通知给 Flutter 框架层。
Flutter Engine 渲染驱动 Flutter 框架
这一部分内容很难以理解,但是这就是真实的逻辑。
除了以下几种情况, Flutter 框架的代码执行都是由 Flutter 引擎触发的。
  • 手势 (屏幕上的事件)
  • 平台消息(如 GPS)
  • 硬件消息(如旋转屏幕, 应用压后台,内存不足等)
  • 异步消息( Future API 或者 HTTP 响应)
注:
一般情况下,如果Flutter渲染引擎没有发出通知, Flutter 框架是不能更新任何UI的。
有些时候,在没有 Flutter 渲染引擎通知的情况下,也可以让  Flutter 框架更新UI,但是并不建议这么做。
你或许会问我,执行手势相关的逻辑,会让 UI 发生变化;使用一个异步任务,或者动画,也会让 UI 发生改变,那它们又是如何工作的呢?
如果你想更新UI, 或者说你想在后台执行代码逻辑并更新 UI,你需要告诉 Flutter 引擎,这里有一些更改需要被渲染到屏幕上。通常情况下,在屏幕下一次刷新的时候, Flutter引擎会通知 Flutter框架,让它来提供新场景的图像来进行渲染显示。
因此,Flutter 引擎是如何基于渲染编排整个应用程序的行为?
从下面的动画中,我们可以了解到整个内部运行机制:
640?wx_fmt=gif
整个动画过程解释如下:
  • 像手势、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 框架执行完所有的任务并且在屏幕中渲染完成。
  • 紧接着会继续一遍又一遍的执行上述流程。
RenderView  和 RenderObject
在讨论与事件流相关的细节之前,先来看看视图树。
如之前所说, 所有东西最后都会转换成像素显示在屏幕中, Flutter 框将我们用于开发程序使用的 Widgets 转化成可视部分,显示在屏幕上。
在 Flutter 中,用来与渲染在屏幕上的可见视图一一对应的对象,我们称作 RenderObject,它被用来表示:
  • 定义屏幕中的区域,包括 大小,位置, 几何结构。也可称其为" 渲染内容"。
  • 识别可能受到手势影响的屏幕区域。
一堆 RenderObject 共同组成了一棵树,称之为 视图树。在视图树的最上面,也就是其跟节点,就是 RenderView。 RenderView 代表了整个输出的视图树,它也是一种特殊的 Renderobject , 如图所示:

640?wx_fmt=png

 
在文章的后面会讲解 WidgetsRenderObjects 之前的关系。但在这之前,我们需要更深入的了解 RenderObjects
 
Bindings的初始化
当 Flutter 应用程序启动的时候,系统会执行 main() 方法,它会调用 runApp(Widget app)
在调用runApp()这个方法的时候,Flutter 会初始化 Flutter 框架Flutter 引擎 之间的接口,它被称作是 bindings。
Bindings简介
Bindings 是建立 Flutter 框架Flutter 引擎之间通信的桥梁,Flutter的这两个部分只能通过它传递数据(其中RenderView 是个例外,在后面会讲到)。
每个 bindings负责处理一组特定的任务、操作或者事件。本文中,作者按其作用域进行了重新分组。
到目前为止, Flutter 框架 提供8个 bindings,本文中,只讨论以下四个:
  • SchedulerBinding
  • GestureBinding
  • RendererBinding
  • WidgetsBinding
为了保证完整性,我列出剩下四个   bindings:
  • ServicesBinding :处理不同平台发过来的消息
  • PaintingBinding:处理图片缓存
  • SemanticsBinding:保留到以后实现所有与语义相关的内容
  • TestWidgetsFlutterBinding:组件测试使用的
当然,我也注意到了 WidgetsFlutterBinding ,但这个不是真正的 bindings, 而是一种 bindings 初始化工具。
有关 bindings 与 Flutter 引擎的交互逻辑,见下图所示:

640?wx_fmt=png

下面,我们分别来看一下这几个 bindings。
SchedulerBinding
它有两个主要功能:
  • 第一个是告诉 Flutter 引擎:“我现在已经准备好了,在你不忙的时候,把我唤醒,告诉我要渲染的内容,我会开始工作。”
  • 第二个是监听并响应一些事件,如唤醒事件。

 

当SchedulerBinding接受到唤醒事件 的时候,要做些什么呢?

  • 需要 Ticker 来控制的时候

 

举个例子,假设您有一个动画,并且已经开始执行了。这个动画是由Ticker进行控制的,它需要以固定时间间隔触发回调。要让这样的回调运行,我们需要告诉Flutter 引擎在下次刷新时唤醒我们(发送Begin Frame),触发回调,执行动画任务。在该动画任务结束时,动画还需要继续,它将再次调用SchedulerBinding来调度另一帧。

  • 更改布局

 

当你响应导致视觉变化的事件(例如,更新屏幕的一部分的颜色,滚动,向屏幕中添加/从屏幕中删除某些内容)时,我们需要采取必要的步骤来保证它可以正常的显示在屏幕上。在这种情况下,Flutter框架将调用SchedulerBinding来告诉Flutter Engine去调度另一帧。

GestureBinding

这个 binding 处理手势事件,并与 Flutter 引擎进行通信。它负责接受与手指有关的数据,并确定屏幕的哪一部分受到手势的影响。然后,它会通知这些部分,来响应事件。

RendererBinding

它是 Flutter 引擎与视图树之前的桥梁,它有两个不同的功能:

  • 第一个是监听Flutter引擎发出的消息,当设备设置发生更改,它会告知用户受到影响的视觉效果/语义。
  • 第二个是为 Flutter 引擎提供要显示在屏幕上的数据。

 
为了提供要在屏幕上呈现的修改,它负责驱动PipelineOwner并初始化RenderView。
PipelineOwner是一种协调器,它知道哪个RenderObject需要做一些与布局有关的事情并协调这些动作。
WidgetsBinding
它用于监听用户设置的更改,如语言的修改。不仅如此, WidgetsBinding 否是 Widgets 与 Flutter 引擎之间通信的桥梁,有两个主要的功能:
  • 第一个是负责处理Widgets结构变更的过程;
  • 第二个是触发渲染事件。

 

一些小组件的结构更改是 BuildOwner 来完成的,它跟踪需要重建的小部件,并处理应用于整个小部件结构的其他任务。

 

640?wx_fmt=png

第二部分:Widgets 转换成像素

 

基础的内部原理讲解完成,下面我们来看看 Widgets

在所有有关 Flutter 的文档中,你能看到这样子的描述:在Fluter中,所有的对象都是 Widgets 。这样子说虽然没错,但要说得更精确一些,我觉得应该是:

开发人员的角度来看,在布局和交互方面,与用户界面相关的所有内容均通过Widgets完成。
为啥要如此精确?Widget允许开发人员根据尺寸,内容,布局和交互性来定义屏幕的一部分。不仅这些,还有更多的东西也是通过它来定义。那么,什么是Widgets呢?
不变的属性
在读 Flutter 的源码的时候,你可以在 Widget 类中看到如下定义:
 
1@immutable
2abstract class Widget extends DiagnosticableTree {
3  const Widget({ this.key });
4
5  final Key key;
6
7  ...
8}
这是什么意思?
@immutable 这个注解告诉我们,在 Widget 类中定义的所有变量都是 FINAL 的。换句话说,它们只能被定义一次。因此,一但初始化完成,这个 Widget 的内部变量将不能被修改。
Widgets 结构
当你在使用用Flutter的时候,你写一个页面,会用到 Widget,像下面的代码中那样,定义出要显示的UI:
 
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}
上述例子中,一共使用了7个 Widgets,它们组成了一个树状结构。一个非常简单的结构,根据代码,可以画出如下结构图:

640?wx_fmt=png

正如你看到的,他像一个树, SafeArea 是这个树的根节点。
复杂的 Widget 结构
正如你知道的那样, Widget 可以将很多其它的Widget 聚合在一起。举个例子,我可以使用如下代码替换上面的代码:
 
1Widget build(BuildContext context){
2    return MyOwnWidget();
3}
我们假设 MyOwnWidget 会自己去渲染 SafeArea, Scaffold 等 Widget。这个例子,要表达的意思是:
Widget 可能是一个页子节点,也可能是一颗树。
元素
为什么我会提到这个,正如我们将在后面看到的那样,为了能够生成可以在设备上渲染的图像的像素,Flutter需要详细了解组成屏幕的所有Widget,并确定所有部分, 按要求生成 Widget 。
为了说明这一点,你可以想象一下俄罗斯套娃,最开始的时候,你只能看到一个玩偶,但是这个玩偶中,包含了另一个,然后依次包含另一个,以此内推。

640?wx_fmt=png

Flutter 生成所有的 Widgets 时,就像获得所有的俄罗斯套娃一样。
下图展示了如何显示 Widget 的所有部分,图中,黄色部分表示你在代码中写到的部分,你可以在不同的组件中使用:

640?wx_fmt=png

注:在这里我们使用了 "Widget 树",这只是为了更好的理解逻辑,在 Flutter中并没有这个概念。
现在我们来介绍前面提到的元素。
每个小部件都对应一个元素。元素彼此链接并形成一棵树。因此,元素是树中某个节点的引用。
首先,将元素作为一个节点,它有父结点,也有子结点。然后通过父子关系将它们链接在一起,就可以得到一个树形结构。

640?wx_fmt=png

如图中看到的, 元素可以对应一个 Widget ,也可以对应一个 RenderObject。
总结一下:
  • 没有 Widgets 树,但是有元素树;
  • Widgets 创建 Elements;
  • Element 指向创建它的 Widget;
  • Elements 使用父子关系进行关联;
  • Elements 有一个或多个子节点;
  • Elements 也可以指向一个 RenderObject。
Elements 定义了视图部分的链接关系。
为了可以更好的表达 element 的概念,让我们来看看下图:

640?wx_fmt=png

图中所示, 元素树连接了  Widgets 和 RenderObjects。
但是,为什么  Widget 会创建 Element?
三种 Widgets
在 Flutter , Widgets 可以拆分成三类(仅仅是我个人为了组织它们,而进行的分类),如下:
  • 代理类

 
这类 Widget 主要作用是用来保存一些数据信息的,并且做为树结构的根结点。点型的例子是 InheritedWidget  和 LayoutId这些 Widgets  不会展现出任何的用户页面,但是它们会用来为其它的Widgets提供数据。
  • 渲染类

 
这类 Widget 直接或间接的用于屏幕布局:
  • 大小尺寸

  • UI位置

  • 布局/渲染方式

 
 
 
典型例子: RowColumnStackPaddingAlignOpacityRawImage 等。
  • 组件类

 
剩下的这一类 Widget 不能直接用于大小、位置、布局等设置,它们只是用来展示最终的数据信息。这些 Widget 通常被称之为组件。
典型例子: RaisedButton, Scaffold, Text, GestureDetector, Container。

640?wx_fmt=png  Widgets 分类

为什么拆分显得如此重要?因为根据 Widget 的分类,会关联不同的 Element。
Element 分类
下图中展示了不同的 Element 分类:
640?wx_fmt=png
内部Element 分类
Element 主要分为两大类:
  • 组件,此分类不直接用于任何视觉渲染的部分。
  • 渲染,此分类是直接用于屏幕渲染。
到目前为止,出现了很多的概念,它们是如何关联在一起?
Widgets 和 Elements 是如何在一起工作的?
在Flutter中,整个系统都依赖 Widget / RenderObject 的状态。
更新 element 的状态的两种不同方式:
  • 使用 setState 方法,这个方法可以用于所有的 StatefulElement (注意,我这里面说的不是 StatefulWidget)。
  • 使用通知,基于 ProxyElement来实现状态更新(如: InheritedWidget)。
Elements 的状态变更是需要Elements的引用放入了 脏元素列表中。使RenderObject无效意味着没有任何更改应用于元素的结构,但是发生了renderObject级别的修改:
  • 大小、位置等修改;
  • 需要重绘,如背景颜色修改、字体样式修改。
RenderObject 的状态变更是需要将 RenderObject 的引用放在重绘/重建列表中。
不管是什么级别的变更,只要有变更发生, SchedulerBinding 就会发送一个消息到 Flutter 引擎,开始新的一次UI渲染。
当 Flutter 引擎唤醒 SchedulerBinding 的时候,所有的变更都将生效,像魔法一样。
onDrawFrame()
在前面,我们提到了 SchedulerBinding 两个主要的功能,其中一个就是处理由 Flutter 引擎发出的与帧视图重建相关的请求。下面,我们来看看有关它的详细细节。
SchedulerBinding 收到 Flutter 引擎发出的 onDrawFrame 时,执行的流程图如下所示:
640?wx_fmt=png流程图
  • 第1步,元素

WidgetsBinding被执行的时候, Flutter引擎首先考虑的是元素的变化。因为由建造者自己管理元素树,所以绑定控件的时候,会调用建造者的 buildScope 方法。这个方法中,会将要更改的元素存起来,稍后触发他们进行重建。

rebuild() 主要的原则如下:

  • 大部分时候,触发元素的重建,会调用控件的 build()方法(Widget build(BuildContext context) {….}),这个方法会返回一个新的控件。

 
  • 如果元素没有子节点,这个元素就被创建完成,反之,会先创建子节点。

 
  • 将新控件与元素引用的子控件进行比较:

 
  • 如果可以被替换, 则更新,并保留子控件;

 
  • 如果不可以被替换,子控件会被移除,并创建一个新的。

 
创建一个新的控件会创建一个与之对应的新的元素。并且会把元素插入到元素树中去。
下图展示了这个过程:

640?wx_fmt=png

当控件被创建的时候,需要创建一个元素与之进行关联。
控件与元素的对应关系如下:

640?wx_fmt=png

举个例子:
StatefulElement 在初始化的时候会执行 widget.createState() 方法,创建并关联对应的状态。 RenderObjectElement 在元素被加载的时候,会创建一个 RenderObject, 并且会将这个对象加入到渲染树。
  • 第2步,渲染对象

 
一旦完成了 脏元素有关的所有动作,元素树就变成了一个稳定结构,是时候考虑渲染到屏幕上了。
渲染绑定用来处理渲染树,控件绑定会调用渲染绑定的 drawFrame 方法。
下图展示了 drawFrame() 的整个调用流程:

640?wx_fmt=png

这个过程中,主要执行以下的几个事件:
  • 为每一个标记为的渲染对象计算新的布局(计算大小和几何形状);

  • 使用渲染层,将所有需要重绘的对象重画出来;

  • 将生成的场景数据发送给 Flutter 引擎,然后在屏幕中显示出来;

  • 最后,Semantics  被发送更新到 Flutter 引擎。

 
 
 
 
在流程的最后,设备屏幕显示的图像将会被更新。
手势处理
手指在屏幕上点击或移动,触发手势,这些会被 GestureBinding 处理响应。
当 Flutter 引擎通过 window.onPointerDataPacket 方法发送出手势相关的事件, GestureBinding 会拦截并处理:
  • Flutter 引擎将屏幕位置转换成对应的坐标;

  • 拿到坐标上所有渲染出来的View对应的 RenderObject;

  • 然后遍历所有的RenderObject ,并把对应事件分发给他们;

  • RenderObject 会等待它能处理的时间并处理它。

 
 
 
 
动画
最后一部分,我们将聚焦到动画和 Ticker。
当你初始化一个动画的时候,你通常会创建一个 AnimationController 或者类似的控件或者组件。
在Flutter中,与动画相关的所有内容均为Ticker。
Tikcer 只做一件事情,它在 SchedulerBinding 上注册一个回调,当Flutter引擎在下一次可用的时候,会将它唤醒。
当 Flutter 引擎可用,它会触发 SchedulerBinding 的 "onBeginFrame" 通知。SchedulerBinding 会遍历并执行所有的 Ticker 的回调。
Ticker 会被对此事件感兴趣的拦截拦截,并对其进行处理。当动画执行结束, Ticker会被标识为“不可用”。因此,Ticker 会告知 SchedulerBinding 去执行另一个回调。
总流程图
现在,我们已经知道了 Flutter 内部的工作原理,来看看整体的流程图:
640?wx_fmt=png 总结构图
BuildContext
最后,如果你还记得不同元素类型的图,你可能已经注意到了元素的签名:
dart abstract class Element extends DiagnosticableTree implements BuildContext { ... }
什么是 BuildContext ?
BuildContext是一个接口,它定义一系列元素要实现的方法。
特别的是, BuildCotext 中的 build() 方法在 StatelessWidgetStatefulWidget 中被使用,还有状态对象在 StatefulWidget 中被使用。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
除了一下的两种情况下,其他时候 BuildContext 是没有任何用处的:
  • 控件被重建的时候;
  • 在StatefulWidget链接到你引用的上下文变量的状态。
这就意味着,大部分时候,我们并不需要知道它。
BuildContext 可以用来干什么?
由于BuildContext既与控件相关的元素相对应,也与空间所在树中的位置相对应,因此该BuildContext对以下情况非常有用:
  • 获得对应于控件的渲染对象的基准;

  • 获取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() 方法。

 

640?wx_fmt=png

结语

 

我认为了解Flutter的架构是很有趣的,所有东西都被设计为高效,可扩展且对将来的扩展开放。而且,诸如Widget,Element,BuildContext,RenderObject之类的关键概念并不总是显而易见。

我希望本文对你有用。

原文:https://www.didierboelens.com/2019/09/flutter-internals/
本文为 CSDN 翻译,转载请注明来源出处。

 

【END】

这几个Python技能实战,能让你少些1000行代码!

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

640?wx_fmt=jpeg

 热 文 推 荐 

 

 

 

 

 

640?wx_fmt=gif点击阅读原文,参与中国开发者现状调查问卷!

640?wx_fmt=png
你点的每个“在看”,我都认真当成了喜欢
Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐