Flutter内部原理

Flutter在内部实际上是如何工作的?

什么是Widget、 Element、 BuildContext、 RenderOject、 Binding…

难度: 初学者

简介

去年,当我开始进入传说中的Flutter世界时,与今天相比,当时在互联网上几乎找不到资料。 现在尽管撰写了许多文章,但很少有人谈论Flutter的实际工作原理。

Widget、Element、BuildContext终究是什么? 为什么Flutter运行流畅,为什么有时运行效果与预期不同? 什么是树?

在编写应用程序时,在95%的情况下,只与Widget打交道,显示某些内容或与屏幕进行交互。 但是你是否从未想过所有这些神奇之处实际上是如何运行的? 系统如何知道何时更新屏幕,以及需要更新哪些部分?


第一部分1:背景

这篇文章的第一部分介绍了一些关键概念,这些概念是用来更好的理解这个帖子的第二部分。


回到设备上来

这一次,让我们从头开始,回到基础。

当您查看设备时,或更具体地说,查看设备上运行的应用程序时,只会看到一个屏幕。

实际上,您所看到的只是一系列像素,这些像素共同构成一个平面图像(2维),并且当您用手指触摸屏幕时,设备只能识别手指在玻璃板上的位置。

该应用程序的所有神奇之处(从视觉角度而言),在于使平面图像在大多数情况下基于与以下对象的交互来更新:

  • 设备屏幕(例如屏幕上的手指)
  • 网络(例如与服务器通信)
  • 时间(例如动画)
  • 其他外部传感器

硬件显示设备)可确保在屏幕上渲染图像,该硬件会定期(通常每秒60次)刷新显示。此刷新频率也称为“ 刷新频率”,以Hz(赫兹)表示。

显示设备GPU图形处理单元)接收要在屏幕上显示的信息,该图形处理器是一种专用电子电路,经过优化和设计可从某些数据(多边形和纹理)快速生成图像。 GPU每秒能够生成要显示的“图像”(= 帧缓冲)并将其发送到硬件的次数称为“帧率”。这是用fps单位测量的(例如每秒60帧或60 fps)。

您可能会问我,为什么我从GPU /硬件和物理传感器等等渲染的二维平面图像的概念开始,以及,它与通常的Flutter Widget有什么关系?

仅仅因为Flutter应用程序的主要目标之一,就是合成二维平面图像并与之交互,所以我认为如果我们从这个角度来看Flutter的工作原理,可能会更容易理解。

…而且因为在Flutter中,信不信由你,几乎所有事情都是由必须在正确的时机刷新屏幕的需求所驱动的!


代码和物理设备之间的接口

总有一天,对Flutter感兴趣的每个人都已经看到了以下图片,描述了Flutter的高层次的体系结构。

Flutter Architecture (c) Flutter

当使用Dart编写Flutter应用程序时,我们是在Flutter框架的级别(绿色)。

Flutter框架通过名为Window的抽象层与 Flutter引擎(蓝色)交互。 此抽象层公开了一系列API,可与设备间接通信。

Flutter引擎也是通过此抽象层以如下方式通知Flutter框架的:

  • 感兴趣的事件发生在设备级别(方向更改,设置更改,内存问题,应用程序运行状态…)
  • 一些事件发生在屏幕上(=手势)
  • 平台通道发送一些数据
  • 而且主要是在Flutter引擎准备渲染新帧

Flutter框架由Flutter引擎帧渲染驱动

这种说法很难让人相信,但这是事实。

除了某些情况(请参阅下文),没有由Flutter引擎帧渲染触发的情况下,Flutter框架代码不会被执行。

这些例外是:

  • 手势(=屏幕上的事件)
  • 平台消息(=设备发出的消息,例如GPS)
  • 设备消息(=表示设备状态变化的消息,例如方向,发送到后台的应用程序,内存警告,设备设置…)
  • Futurehttp响应

在Flutter引擎帧渲染未要求的情况下,Flutter框架不会实施任何视觉更改。

(但是,在我们看来,可以不受Flutter引擎的邀请而进行视觉更改,但这确实是不建议

但是您会问我,是否执行了与gesture(手势)相关的某些代码并导致视觉变化发生,或者我是否正在使用timer(计时器)节奏执行某些任务而导致视觉变化(例如动画,举个例子),这该如何运行呢?

如果您希望进行视觉更改,或者希望基于计时器执行某些代码,则需要告诉Flutter引擎,需要渲染某些内容。

通常,在下一次刷新时,Flutter引擎然后会请求Flutter框架运行一些代码,并最终提供要渲染的新场景。

因此,最大的问题是Flutter引擎如何基于渲染来协调整个应用程序的行为?

为了让您了解内部机制,请看以下动画…

简短说明(更多细节将在后面):

  • 某些外部事件(手势,http响应等)甚至future可能会启动一些任务,导致必须更新渲染。消息被发送到Flutter引擎以通知它(= Schedule Frame
  • Flutter引擎准备好进行渲染更新时,它会发出一个Begin Frame请求
  • Begin Frame请求被Flutter框架拦截,后者运行主要与Ticker相关的​​任何任务(例如动画等)
  • 这些任务可能会重新发出请求以供后面渲染帧…(例如:动画为完成并且要继续进行,它需要稍后再接收另一个“开始帧”
  • 然后,Flutter引擎发出一个Draw Frame
  • Draw FrameFlutter框架拦截,后者将查找与更新布局有关的所有任务,这些任务与更新布局结构和大小相关联。
  • 完成所有这些任务后,将继续执行与在绘制方面的布局更新的有关任务。
  • 如果要在屏幕上绘制一些东西,则它将新的Scene渲染到Flutter引擎中,后者将更新屏幕。
  • 然后,Flutter框架执行渲染完成后要运行的所有任务(= PostFrame回调)以及与渲染无关的其他后续任务。
    *…,此流程一次又一次地开始。

RenderView和RenderObject

在深入探讨与动作流程相关的细节之前,是时候介绍Rendering Tree这个概念了。

如前所述,最终所有东西最终变成了要在屏幕上显示的一系列像素,并且Flutter框架将我们正在使用应用程序开发的Widget,转换为将渲染在屏幕上的可视部分。

屏幕上渲染的这些可视部分与称为RenderObject的对象相对应,这些对象用于:

  • 根据尺寸,位置,几何形状以及“渲染内容”定义屏幕的某些区域
  • 识别可能受到手势影响的屏幕区域(= 手指

所有RenderObject的集合形成一棵树,称为Render Tree。在该树的顶部(= root),我们找到一个RenderView

RenderView代表Render Tree的总输出界面,它本身是RenderObject的特殊版本。

从视觉上讲,我们可以将所有这些表示如下:

RenderView - RenderObject

Widgets和RenderObject之间的关系将在本文后面讨论。

现在该深入一点了……


重要的事优先 - 绑定的初始化

启动Flutter应用程序时,系统将调用main()方法,该方法最终将调用runApp(Widget app)方法。

在对runApp()方法的调用期间,Flutter框架初始化Flutter框架Flutter引擎之间的接口。 这些接口称为 绑定

绑定 - 简介

绑定Flutter引擎Flutter框架之间的某种胶水。只有通过这些绑定,才能在Flutter的两个部分(引擎和框架)之间交换数据。
(此规则只有一个例外:RenderView,但我们稍后会看到)

每个绑定负责处理一组特定的任务,动作,事件,并按活动领域重新分组。

在撰写本文时,Flutter框架包含8个绑定。

下面是本文将要讨论的4个:

  • SchedulerBinding
  • GestureBinding
  • RendererBinding
  • WidgetsBinding

为了完整起见,最后4个(本文将不介绍):

  • ServicesBinding:负责处理平台通道发送的消息
  • PaintingBinding:负责处理图像缓存
  • SemanticsBinding:保留供以后实现与Semantics相关的​​所有内容
  • TestWidgetsFlutterBinding:由widget测试库使用

我还可以提到WidgetsFlutterBinding,但后者实际上不是绑定,而是某种“ 绑定初始化器 ”。

下图显示了我将在本文稍后介绍的绑定与Flutter引擎之间的交互。

Bindings interactions

让我们看一下这些“主要”绑定。


SchedulerBinding

这个绑定具有2个主要职责:

  • 第一个是告诉 Flutter引擎:“嘿!下次您不忙时,叫醒我,以便我可以运行一下,并告诉您要渲染的内容或是否你需要稍后再呼叫我……”;
  • 第二个是监听并响应此类“唤醒呼叫”(请参阅​​稍后的内容)

SchedulerBinding何时请求唤醒呼叫

  • Ticker需要滴答
    例如,假设您有一个动画,然后启动它。动画由Ticker进行节奏控制,并以固定间隔(=滴答)被调用以运行回调。要运行这样的回调,我们需要告诉Flutter引擎在下次刷新时唤醒我们(= Begin Frame)。这将调用ticker回调来执行其任务。在该任务结束时,如果ticker仍需要向前移动,它将调用 SchedulerBinding安排另一个帧。

  • 更改布局时
    例如,当您响应导致视觉变化的事件(例如,更新屏幕的一部分的颜色,滚动,向屏幕中添加某些东西/从屏幕中删除某些东西)时,我们需要采取必要的步骤,最终在屏幕上渲染它。在这种情况下,当发生此类更改时,Flutter框架将调用SchedulerBinding,使用Flutter引擎安排另一个帧。 (我们将在后面看到它的实际运行方式)


GestureBinding

这个绑定以“手指”(= 手势)来表示与引擎的交互。

特别是,它负责接受与手指有关的数据并确定屏幕的哪些部分受到手势的影响。 然后,它会相应地通知此/这些部分。


RendererBinding

这个绑定是* Flutter引擎和 Render Tree之间的粘合剂。 它有2个不同的职责:

  • 第一个是监听引擎发出的事件,告知用户通过设备设置实施的更改,这些更改会影响视觉效果和/或语义
  • 第二个是为引擎提供要应用于显示的修改。

为了提供要在屏幕上渲染的修改,此Binding负责驱动PipelineOwner并初始化RenderView

PipelineOwner是一种协调器,它知道哪些RenderObject需要做一些与布局有关的事情并协调这些动作。


WidgetsBinding

这个绑定可以监听用户通过设备设置实施的更改,这些更改会影响语言(= 区域设置)和语义

旁注

在稍后的阶段,我想所有与Semantic相关的事件都将迁移到SemanticsBinding,但是在撰写本文时,情况还不是这样。

除此之外,WidgetsBinding是Widget与Flutter引擎之间的胶水。 它有2个不同的主要职责:

  • 第一个主要是驱动负责处理Widget结构更改的过程
  • 第二个是触发渲染

Widgets结构更改的处理是通过BuildOwner完成的。

BuildOwner跟踪哪些Widget需要重建,并处理应用于整个Widget结构的其他任务。


第二部分:从Widget到像素

既然我们已经介绍了内部机制的基础。

在所有的Flutter 文档中,你可能读过一切皆Widget

好吧,这几乎是正确的,但是为了更加精确,我宁愿说:

开发人员的角度来看,与用户界面有关的所有布局和交互都是通过小部件完成的。

为什么要这么精确? 因为Widget允许开发人员根据尺寸、内容、布局和交互性来定义屏幕的一部分,还有更多。 那么,什么是Widget呢?


不可变的配置

阅读Flutter源代码时,您会注意到Widget类的以下定义。

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key key;

  ...
}

这是什么意思呢?

注释“@immutable”非常重要,它告诉我们Widget类中的任何变量都必须为FINAL,换句话说:“已定义并分配 只此一次”。 因此,一旦实例化,Widget将不再能够调整其内部变量

Widget是一种常量配置,因为它是IMMUTABLE


Widget层次结构

当您使用Flutter开发时,您可以使用Widget来定义屏幕的结构,例如:

Widget build(BuildContext context){
    return SafeArea(
        child: Scaffold(
            appBar: AppBar(
                title: Text('My title'),
            ),
            body: Container(
                child: Center(
                    child: Text('Centered Text'),
                ),
            ),
        ),
    );
}

该示例使用7个Widget,它们共同构成一个层次结构。 基于代码的非常简化结构如下:

Simplified Widgets Tree

如您所见,这看起来像一棵树,其中SafeArea是树的根。


树后面的森林

如您所知,Widget本身可能是其他Widget的集合。 例如,我可以通过以下方式编写先前的代码:

Widget build(BuildContext context){
    return MyOwnWidget();
}

这里设想Widget“ MyOwnWidget”本身会渲染SafeArea,Scaffold ...,但是此示例最重要的是

Widget可能是叶子,树中的节点,甚至是树本身,或者为什么不是树的森林……


树中的Element概念

我为什么要提这个呢?

正如我们将在后面看到的那样,为了能够生成组成要在设备上渲染的图像的像素,Flutter需要详细了解组成屏幕的所有小部分并确定所有部分 ,它将要求膨胀(inflate)所有Widget

为了说明这一点,请考虑俄罗斯玩偶的原理:关闭时,您只能看到1个玩偶,但后者包含另一个,然后依次包含另一个,依此类推...

Russian dolls

Flutter将所有widget膨胀到屏幕的一部分时,这类似于获取所有不同的俄罗斯玩偶,即全部的一部分。

下图显示了最终Widget层次结构的一部分,与先前的代码相对应。 用黄色突出显示了代码中提到的Widget,以便您可以在生成的局部Widget树中发现它们。

Inflated Widgets

重要说明

“Widget树”的用语仅是为了使程序员易于理解,因为程序员正在使用小部件,但是在Flutter中没有Widget树!

实际上,正确地说,我们应该说:“Element树

现在是时候介绍Element的概念了……

每个小部件对应一个元素。 元素彼此链接并形成一棵树。 因此,元素是树中某物的引用。

首先,将元素视为有父节点并可能子节点的节点。 通过父节点关系链接在一起,我们得到一个树形结构。

An Element

如上图所示,Element指向一个Widget,并且可能也指向一个RenderObject

甚至更好…Element指向创建元素的Widget!

让我回顾一下...

  • 没有Widget树,而是Element树
  • Element是由Widget创建的
  • Element引用创建它的Widget
  • Element与父节点关系链接在一起
  • Element可以有一个子节点或多个子节点
  • Element也可以指向RenderObject

Element定义视觉部件之间如何链接

为了更好地可视化元素的概念适合的位置,我们考虑以下视觉表示:

如您所见,Element树WidgetRenderObject之间的实际链接。

但是,为什么Widget会创建Element?


三个主要Widget类别

Flutter中,Widget分为3个主要类别,我个人称这些类别为(但是这是我将他们分类的方式):

  • 代理

    这些Widget的主要作用是保存一些信息,这些信息需要供Widget使用,Widget是树结构的一部分,以代理为根。此类Widget的典型示例是InheritedWidgetLayoutId

    这些Widget并不直接属于用户界面,而是被其他用来获取其可以提供的信息。

  • 渲染器

    这些Widget与它们定义(或用于推断)的屏幕布局有直接的关联:

    • 尺寸;
    • 位置;
    • 布局,渲染。

    典型示例包括: Row, Column, Stack, 还有Padding, Align, Opacity, RawImage

  • 组件。

    这些是其他Widget,它们不直接提供与尺寸,位置,外观有关的最终信息,而是将用于获取最终信息的数据(或提示)。这些Widget通常被称为组件

    例如:RaisedButton, Scaffold, Text, GestureDetector, Container

以下PDF列出了大多数Widget,按类别重新分组。

为什么拆分很重要? 因为根据Widget类别,关联了一个对应的Element类型...


Element类型

一下是不同的Element类型

如您在上图中所见,元素分为两种主要类型:

  • ComponentElement

    这些元素不直接对应于任何视觉渲染部分。

  • RenderObjectElement

    这些元素对应于渲染屏幕的一部分。

非常好! 到目前为止,有很多信息,但是如何将所有内容链接在一起?为什么要引入所有这些?


Widget和Element如何一起运行?

Flutter中,整个机制依赖于使ElementrenderObject无效(invalidate)

可以通过不同方式使Element失效:

  • 通过使用setState,这会使整个StatefulElement无效(请注意,我故意不说StatefulWidget
  • 通过通知,由其他proxyElement处理(例如InheritedWidget),这会使依赖于proxyElement的任何Element无效

无效的结果是在脏Element列表中引用了相应的Element

使renderObject无效意味着未对Element的结构进行任何更改,而是在renderObject级别上进行了修改,例如

  • 更改其尺寸,位置,几何形状...
  • 需要重新绘制,例如,当您仅更改背景颜色,字体样式时…

这种无效的结果是在需要重建或重新绘制的renderObject列表中引用了相应的renderObject

无论哪种失效类型,发生这种情况时,都要求SchedulerBinding(还记得吗?)要求Flutter引擎安排新的

Flutter引擎唤醒SchedulerBinding时,所有的神奇之处都会发生……

onDrawFrame()

在本文的前面,我们提到SchedulerBinding具有2个主要职责,其中之一是准备处理Flutter引擎发出的与帧重建有关的请求。 现在是专注于此的绝佳时机……

下面的部分序列图显示了当SchedulerBindingFlutter引擎接收到onDrawFrame()请求时会发生什么情况。

第一步:Element

WidgetsBinding被调用,后者首先考虑与Element相关的​​更改。

由于BuildOwner负责处理Element树,因此WidgetsBinding会调用buildOwnerbuildScope方法。

此方法迭代无效元素(=脏)的列表,并要求它们进行重建

rebuild()方法的主要原理是:

  1. 请求元素进行rebuild(),这导致大多数情况,调用了该element所引用的Widget的build方法(=方法Widget build(BuildContext context){…})。这个* build()*方法返回一个新的Widget。
  2. 如果元素没有子元素,则将新Widget膨胀(见下文),否则
  3. 将新Widget与该Element的子Element引用的Widget进行比较。
    如果可以交换(= 相同的Widget类型和键),则进行更新,并保留子Element。
    如果无法交换它们,则子Element将被卸载(〜 被丢弃
    ),并且新的Widget会膨胀。
  4. Widget的膨胀导致创建一个新Element,该Element将作为该Element的新Element挂载。 (挂载 =已插入到Element树中)

下面的动画试图使这种解释更加直观。

onDrawFrame() - Elements

Widget膨胀的注意点

当Widget膨胀,要求创建一个由widget类别定义的特定类型的新element

所以,

  • 一个InheritedWidget将产生一个InheritedElement
  • 一个StatefulWidget将生成一个StatefulElement
  • 一个StatelessWidget将生成一个StatelessElement
  • 一个InheritedModel将生成一个InheritedModelElement
  • 一个InheritedNotifier将生成一个InheritedNotifierElement
  • 一个LeafRenderObjectWidget将生成一个LeafRenderObjectElement
  • 一个SingleChildRenderObjectWidget将生成一个SingleChildRenderObjectElement
  • 一个MultiChildRenderObjectWidget将生成一个MultiChildRenderObjectElement
  • 一个ParentDataWidget将生成一个ParentDataElement

每种element类型都有不同的行为。

例如

  • StatefulElement将在初始化时调用widget.createState()方法,这将创建State并将其链接到Element
  • 一个RenderObjectElement类型将创建一个RenderObject,当Element被挂载时,这个renderObject将被添加到render树并链接到element

第二步:renderObject

一旦完成与dirty元素相关的所有操作,element树现在就稳定了,现在是时候考虑渲染过程了。

由于RendererBinding负责处理渲染树,因此WidgetsBinding调用RendererBindingdrawFrame方法。

下面的局部图显示了在drawFrame()请求期间执行的操作顺序。

在此步骤中,执行以下活动:

  • 要求每个标有dirtyrenderObject进行布局(意味着计算其尺寸和几何形状)
  • 使用renderObject的图层对每个标记为“需要绘制”的renderObject进行重新绘制。
  • 生成的scene被构建并发送到Flutter引擎,以便后者将其发送到设备屏幕。
  • 最后语义也被更新并发送到Flutter引擎

在该操作流程结束时,将更新设备屏幕。


第三步:手势处理

手势(=与屏幕上的手指有关的事件)由GestureBinding处理。

Flutter引擎通过window.onPointerDataPacket API发送与手势相关的事件的信息时,GestureBinding会拦截它,并进行一些缓冲,并:

  1. 转换Flutter引擎发出的坐标,以匹配设备像素比率,然后
  2. 请求renderView提供所有RenderObject的列表,该列表覆盖了包含事件坐标的屏幕的一部分
  3. 然后迭代该renderObject列表,并将相关事件分派给它们中的每一个。
  4. renderObject正在等待此类事件时,它将对其进行处理。

从这个解释中,我们直接看到renderObject有多重要...


第四步:动画

本文的最后一部分重点关注动画的概念,更具体地说是Ticker的概念。

启动动画时,通常使用 AnimationController或任何类似的Widget或组件。

Flutter中,与动画相关的所有内容均指Ticker的概念。

激活时,Ticker只做一件事:“ 它请求 SchedulerBinding注册回调,并要求Flutter引擎在下一个可用回调时将其唤醒”。

Flutter引擎准备就绪时,它会通过以下请求调用SchedulerBinding:“ onBeginFrame”。

然后,SchedulerBinding拦截此请求,然后遍历ticker回调的列表并调用它们中的每一个。

每个ticker滴答都会被对此事件感兴趣的任何控制器拦截以对其进行处理。如果动画已完成,则ticker为“disabled”,否则代码为“ SchedulerBinding”请求安排另一个回调。等等…


全局图

现在我们已经了解了Flutter内部的工作原理,这是全局图:

Internals Big Picture

BuildContext

最后的话...

如果您回想起显示不同element类型的图,您很可能会注意到基础Element的签名:

abstract class Element extends DiagnosticableTree implements BuildContext {
    ...
}

这就是著名的 BuildContext.

什么是BuildContext呢?

BuildContext是一个接口,它定义了可以由Element以协调的方式实现的一系列getter方法

特别是,BuildContext主要用于StatelessWidgetStatefulWidgetbuild()方法或StatefulWidgetState对象中。

BuildContext不是别的,只是元素本身对应的

  • 重建的Widget(在build或builder方法内部)
  • StatefulWidget链接到引用上下文变量的State

这意味着大多数开发人员甚至在不知情的情况下也不断地处理 element

BuildContext有用吗?

由于BuildContext既与widget相关的​​ element也与树中widget的位置相对应,因此BuildContext对以下情况非常有用:

  • 获取与widget对应的RenderObject的引用(或者如果widget不是renderer,则*子孙widget
  • 获取RenderObject的大小
  • 访问树。实际上所有通常实现of方法的widget都使用此方法(例如MediaQuery.of(context), Theme.of(context)…

只是为了好玩……

现在我们已经了解到BuildContextelement,出于乐趣,我想向您展示另一种使用它的方式...

以下无用代码使StatelessWidget可以自我更新(就像它是StatefulWidget,但不使用任何setState()),通过使用BuildContext...

void main(){
    runApp(MaterialApp(home: TestPage(),));
}

class TestPage extends StatelessWidget {
    // final because a Widget is immutable (remember?)
    final bag = {"first": true};

    @override
    Widget build(BuildContext context){
        return Scaffold(
            appBar: AppBar(title: Text('Stateless ??')),
            body: Container(
                child: Center(
                    child: GestureDetector(
                        child: Container(
                            width: 50.0,
                            height: 50.0,
                            color: bag["first"] ? Colors.red : Colors.blue,
                        ),
                        onTap: (){
                            bag["first"] = !bag["first"];
                            //
                            // This is the trick
                            //
                            (context as Element).markNeedsBuild();
                        }
                    ),
                ),
            ),
        );
    }
}

在我们之间,当您调用setState()方法时,后者最终会做同样的事情:_element.markNeedsBuild().


结论

又是一篇很长的文章,不是吗

我想这是很有趣的,知道Flutter是如何构建的,并提醒所有东西都被设计为高效、可扩展且对将来的扩展开放。

此外,诸如WidgetElementBuildContextRenderObject之类的关键概念并不总是显而易见的。

我希望本文可能有用。

敬请期待新文章。 同时,祝您编码愉快。

推荐阅读更多精彩内容