颤振如何工作


Flutter实际如何运作?


什么是窗口小部件,元素,BuildContext,RenderOject,绑定?


难度: 初学者


参赛作品


去年( 注:在2018年 ),当我开始进入神话般的Flutter世界时,与今天相比,互联网上的信息很少。 现在,尽管已经编写了许多材料,但其中只有一小部分谈论Flutter的实际工作原理。


什么是小部件( widgets ),元素( elements ),BuildContext? 为什么Flutter快速? 为什么有时它不能按预期工作? 什么是树木,为什么需要它们?


在95%的情况下,编写应用程序时,您将只处理小部件以显示某些内容或与之交互。 但是,您是否从未真正想过所有这些魔术如何发挥作用? 系统如何知道何时刷新屏幕以及应更新哪些部分?


内容:



第1部分:背景


本文的第一部分介绍了一些关键概念,这些概念将在本材料的第二部分中使用,并有助于更好地理解Flutter。


关于设备的一点点


让我们从头开始,再回到基础。


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


实际上,您所看到的只是像素,它们共同构成一个二维图像,并且当您用手指触摸屏幕时,设备仅会识别手指在玻璃板上的位置。


在大多数情况下(从视觉角度而言),应用程序的所有奇妙之处在于,它基于以下交互作用来更新该图像:


  • 使用设备屏幕( 例如,玻璃上的手指
  • 与网络( 例如,与服务器通信
  • 随着时间的流逝( 例如动画
  • 与其他外部传感器

屏幕上图像的可视化由硬件(显示器)提供,该硬件定期(通常每秒60次)更新显示。 这称为“刷新率”,以Hz(赫兹)表示。


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


您可能会问我,为什么我以GPU /硬件和物理玻璃传感器显示的二维图像的概念开始本文,以及与常规Flutter小部件之间的关系是什么?


我认为,如果从这种角度来看,将更容易理解Flutter的实际工作原理,因为Flutter应用程序的主要目标之一是创建此二维图像并使其与之交互。 也因为不相信,在Flutter中,几乎所有事情都是由于需要在正确的时间快速更新屏幕!


代码与设备之间的接口


无论如何,对Flutter感兴趣的每个人都已经看到了以下描述Flutter 高级架构的图片



当我们使用Dart编写Flutter应用程序时,我们仍处于Flutter Framework级别(以绿色突出显示)。


Flutter框架通过称为Window的抽象层与Flutter Engine (蓝色)进行交互。 这种抽象级别提供了许多用于与设备进行间接交互的API。


同样通过这种抽象级别, Flutter引擎在以下情况下通知Flutter框架


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

管理Flutter Framework Flutter Engine渲染


很难相信,但这是事实。 除了某些情况( 请参阅下文 ),在不启动Flutter Engine渲染的情况下不会执行Flutter Framework代码。


例外情况:


  • 手势/手势(=玻璃上的事件)
  • 平台消息(=由设备生成的消息,例如GPS)
  • 设备消息(=与设备状态更改有关的消息,例如方向,在后台发送的应用程序,内存警报,设备设置...)
  • 未来或http回应

(在我们之间,您实际上可以应用视觉更改,而无需通过Flutter Engine调用,但这是不推荐的


您问我:“如果执行了与手势相关的某种代码并引起视觉变化,或者如果我使用计时器来设置导致视觉变化的任务的频率(例如动画),那么它如何工作?”


如果希望发生视觉变化或基于计时器执行某些代码,则需要告诉Flutter Engine需要绘制一些内容。


通常,下次Flutter Engine更新时,它会调用Flutter Framework执行一些代码,并最终提供一个新的呈现场景。


因此,一个重要的问题是Flutter引擎如何基于渲染组织所有应用程序行为。


要了解内部机制,请看以下动画:



简要说明(稍后将提供更多详细信息):


  • 某些外部事件(手势,http响应等)或什至是将来,都可能触发需要更新显示的任务。 相应的消息被发送到Flutter Engine (= Schedule Frame
  • Flutter Engine准备开始更新渲染时,它将创建一个Begin Frame请求。
  • Flutter Framework截获了此Begin Frame请求,该框架执行主要与代码相关的任务(例如,动画)
  • 这些任务可以重新创建请求以供以后渲染(例如:动画尚未完成其执行,并且要完成该动画,它需要在以后的阶段中获得另一个Begin Frame )。
  • 接下来, Flutter引擎发送并条机,并条机将其捕获 ,并在结构和大小方面查找与更新布局有关的所有任务
  • 完成所有这些任务后,他继续进行与在渲染方面更新布局相关的任务
  • 如果屏幕上需要绘制某些内容,则将用于可视化的新场景( Scene )发送到Flutter Engine ,后者将更新屏幕
  • 然后Flutter框架执行渲染后将执行的所有任务(= PostFrame回调),以及与渲染无关的任何其他后续任务
  • ...然后这个过程重新开始

RenderView和RenderObject


在深入研究工作流的细节之前,是时候介绍“ 渲染树”的概念了。


如前所述,最终所有内容都将转换为将在屏幕上显示的像素, Flutter框架会将我们用于开发应用程序的小部件转换为将在屏幕上显示的可视块。


这些可视部分对应于称为RenderObject的对象,这些对象用于:


  • 根据大小,位置,几何形状以及“渲染内容”定义屏幕的特定区域
  • 识别可能受手势影响的屏幕区域(=手指触摸)

一组所有RenderObject组成一棵树,称为Render Tree 。 在这棵树的顶部(= ),我们找到一个RenderView


RenderViewRender Tree对象提供了公共表面,并且是RenderObject的特殊版本。


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


WidgetRenderObject之间的关系将在后面讨论。 同时,该深入一点了。


初始化绑定


当Flutter应用程序启动时,首先调用main()函数,最终调用runApp(Widget app)方法。


runApp()方法时runApp() Flutter框架将初始化其自身与Flutter Engine之间的接口。 这些接口称为绑定请注意:bindings )。


绑定介绍


绑定被设计为框架和Flutter引擎之间的链接。 只有通过绑定,才能在Flutter框架Flutter引擎之间交换数据。
(该规则只有一个例外 -RenderView ,但我们将在后面讨论)。


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


在撰写本文时, Flutter框架具有8个绑定。


下面是他们的4,这将在本文中进行讨论:


  • 调度程序绑定
  • 手势绑定
  • 渲染器绑定
  • 小部件绑定

为了完整起见,我将提及其余的4个:


  • ServicesBinding:负责处理消息发送给信道的平台平台通道构件
  • PaintingBinding :负责处理图像缓存
  • SemanticsBinding :保留用于与语义相关的所有内容的后续实现
  • TestWidgetsFlutterBinding :由窗口小部件测试库使用

您也可以提到WidgetsFlutterBinding ,但这并不是一个绑定,而是一种“绑定初始化器”


下图显示了绑定之间的互动,我会进一步考虑和颤振引擎。



让我们看一下这些“核心”绑定。


调度程序绑定


此绑定有两个主要职责:


  • 说说Flutter Engine“嘿!下次当您不忙时,叫醒我,这样我就可以工作一点,告诉您要渲染什么,或者是否需要您以后再打电话给我……”
  • 倾听并回应这种“令人不安的觉醒” (见下文)

SchedulerBinding何时请求唤醒电话


  • 股票代号必须算出新的代号时


    例如,您有一个动画,然后启动它。 使用“ 股票行情指示器”对动画进行裁剪,该行情发生器会定期调用(= tick )以执行回调 。 为了启动这样的回调 ,我们需要告诉Flutter Engine,以便在下一次更新(= Begin Frame )时唤醒我们。 这将启动自动收录器回调以完成其任务。 如果代码仍然需要继续执行,则在其任务结束时,它将调用SchedulerBinding来调度另一个框架。


  • 何时更新显示


    例如,我们需要制定一个导致视觉变化的事件(例如:更新屏幕一部分的颜色,滚动,在屏幕上添加/删除某些东西),为此,我们需要采取必要的步骤以最终在屏幕上显示更新的图像。 在这种情况下,当发生此类更改时, Flutter框架将调用SchedulerBinding以使用Flutter Engine调度另一个帧。 (稍后我们将了解其实际工作原理)



手势绑定


此绑定根据“手指” (= 手势 )侦听与引擎的交互。


特别是,他负责接收与手指有关的数据,并确定手势在屏幕的哪一部分使用。 然后,他相应地通知这些部分。


渲染器绑定


此绑定是Flutter EngineRender Tree之间的链接。 她负责:


  • 侦听引擎生成的事件,以通知用户通过影响视觉效果和/或语义的设备设置应用的更改
  • 向引擎发送有关将应用于显示的更改的消息

为了提供将在屏幕上显示的更改, RendererBinding负责管理PipelineOwner并初始化RenderView


PipelineOwner是一种乐团 ,它知道根据组件使用RenderObject需要执行的操作,并协调这些动作。


小部件绑定


该绑定侦听用户通过影响语言(= 语言环境 )和语义的设备设置应用的更改。


小笔记

我假设在 Flutter 开发的后期所有与语义相关的事件都将转移到 SemanticsBinding ,但是在撰写本文时,情况并非如此。

另外, WidgetsBinding是小部件和Flutter Engine之间的链接。 她负责:


  • 管理小部件结构更改的处理过程
  • 渲染通话

处理部件结构通过使用改变BuildOwner


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


第2部分。从小部件到像素


既然我们已经了解了Flutter内部工作的基础知识,那么该讨论小部件了。


在所有Flutter文档中,您将阅读所有小部件 (小部件)。


这几乎是正确的。 但是为了更精确一点,我宁愿说:


从开发人员的角度来看,在布局和交互方面,与用户界面相关的所有内容均使用小部件完成。

为什么这么准确? 除了Widget允许开发人员根据大小,内容,布局和交互性来确定屏幕的一部分外,还有很多其他功能。 那么Widget到底是什么?


不变的配置


如果您查看Flutter的源代码,您会注意到Widget类的以下定义。


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

这是什么意思?


注释“ @immutable”非常重要,它告诉我们Widget类中的任何变量都必须为FINAL ,换句话说:“ 为每个人定义并分配一次” 。 因此,在创建实例后,小部件将不再能够更改其内部变量。


由于Widget是不可变的,因此可以将其视为静态配置。

小部件的层次结构


使用Flutter设计时,您可以使用如下小部件定义屏幕的结构:


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

本示例使用7个小部件,这些小部件共同形成一个层次结构。 基于此代码的非常简化的方案如下:



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


树木背后的森林


如您所知,小部件本身可以是其他小部件的集合。 例如,您可以按以下方式修改前面的代码:


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

此选项假定小部件“ MyOwnWidget”本身将显示SafeAreaScaffold 。 但是在这个例子中最重要的是


小部件可以表示叶子,树中的结,甚至是树本身,或者为什么代表森林?

了解树中的元素


这和它有什么关系?


如稍后所示,为了能够生成组成设备上显示图像的像素, Flutter必须详细了解组成屏幕的所有小部分,并且要确定所有部分,需要了解所有小部件的扩展


为了说明这一点,考虑娃娃的原理:在一个封闭的状态下,你看到的只是一个娃娃,但她还有另一个,又包含另一种,等等...



Flutter展开所有小部件(屏幕的一部分)时 ,就像获取所有娃娃(整个 屏幕的 一部分)一样


下面的图片示出了对应于先前代码窗口小部件的分层结构的最后部分。 用黄色突出显示了前面代码中提到的小部件,以便您可以在最后的树中定义它们。



重要说明

语言“小部件树”的存在只是为了便于理解,因为程序员使用小部件,但是Flutter中没有小部件树!

实际上,说“元素之树”会更正确

现在该介绍Element的概念了。


每个小部件都有一个元素。 元素相互连接并形成一棵树。 因此, 元素是对树中某物的引用。

首先,将元素视为具有父级(可能还有子级)的节点。 通过父子关系将它们链接在一起,我们得到了树形结构。



如您所见,该元素指向一个小部件,也可以指向RenderObject


更好的是... Element指向创建此Element的Widget!

让我们总结一下:


  • 没有树小部件,但有一个木质元素
  • 元素是小工具
  • 该项目是指创建它的小部件。
  • 链接到父级关系的元素
  • 一个项目可能有一个“婴儿”。
  • 元素也可以指向RenderObject。

元素确定显示的块的各个部分如何相互关联。

为了更好地了解一个办法概念,让我们考虑下面的可视化表示:



如您所见,元素树是小部件和RenderObjects之间的实际关系。


但是,为什么Widget创建一个Element


3种小部件


在Flutter中,小部件分为3类,我个人将其称为(但这只是我对它们进行分类的方式)


  • 代理人


    这些窗口小部件的主要目的是存储一些信息(窗口小部件应可访问这​​些信息),这些信息是基于Proxy的树结构的一部分。 此类小部件的一个示例是InheritedWidgetLayoutId


    这些小部件不直接参与用户界面的形成,而是用于获取它们可以提供的信息。


  • 渲染


    这些小部件与屏幕的布局直接相关,因为它们确定(或用于确定) 大小位置渲染 。 典型示例包括: RowColumnStack以及PaddingAlignOpacityRawImage ...


  • 组成部分


    这些是其他小部件,它们不直接提供与大小,位置,外观有关的最终信息,而是提供将用于获取相同最终信息的数据(或技巧)。 这些窗口小部件通常称为组件。


    示例: RaisedButtonScaffoldTextGestureDetectorContainer ...




此PDF文件发送最高按类别分组的小部件。


为什么这种分离很重要? 因为根据窗口小部件的类别,相应的元素类型与...关联。


项目类型


有几种类型的元素:



正如可以在上面的图中看到,该元件被分成两种主要类型:


  • 组成要素


    这些元素不直接负责渲染显示的任何部分。


  • RenderObjectElement


    这些元素负责屏幕上显示的图像部分。



太好了! 这么多的信息,但是所有这些之间是如何关联的,为什么谈论它有趣呢?


小部件和元素如何协同工作


在Flutter中,所有机制都基于使元素或renderObject无效。

元素失效可以通过以下方式完成:


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

无效的结果是,到相应元素的链接出现在元素列表中。


renderObject无效意味着元素的结构完全不变,但是renderObject级别发生了变化,例如:


  • 改变其大小,位置,几何形状...
  • 某些东西需要重新粉刷,例如,当您仅更改背景颜色,字体样式时...

这种无效的结果是链接到需要重建或重新绘制的渲染对象(renderObject)列表中的相应renderObject


不管失效类型引起SchedulerBinding(还记得吗?)要查询扑引擎,所以他计划组建一个新框架。


这是时刻扑引擎 “唤醒” SchedulerBinding发生的所有魔法...


onDrawFrame()


在本文的前面,我们注意到SchedulerBinding有两个主要职责,其中之一是愿意处理Flutter Engine提出的与帧重建有关的请求。 这是专注于此的最佳时机。


下面的部分时序图显示了SchedulerBindingFlutter Engine接收到onDrawFrame()请求时发生的情况。



步骤1.元素


WidgetsBinding,并且这种结合首先考虑与元件相关联的变化。 WidgetsBinding调用buildOwner对象的buildScope方法,因为BuildOwner负责处理项目树。 此方法遍历所有元素并请求对其进行重建


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


  1. 通过调用此元素所引用的窗口小部件的build()方法(=窗口Widget build (BuildContext context) {...}方法),有一个重建该元素的请求(这将花费大部分时间)。 这个build()方法将返回一个新的小部件
  2. 如果该元素没有“子级”,则为新的小部件创建一个元素(请参见下文)( 注意: inflateWidget ),否则
  3. 将新的小部件与元素的子代所引用的小部件进行比较
    • 如果它们是可互换的(= 相同的小部件类型和键 ),则将进行更新并保存子级。
    • 如果他们不能互换,子元素将被丢弃(丢弃〜)和新的窗口小部件创建的元素
  4. 这种新元件被安装为元素的子元素。 (已安装) =插入到元素树中)

下面的动画将尝试使这一解释更加清楚。



注意小部件和元素


对于新的小部件,将创建与小部件类别相对应的特定类型的元素,即:


  • InheritedWidget- > InheritedElement
  • StatefulWidget - > StatefulElement
  • StatelessWidget- > StatelessElement
  • InheritedModel- > InheritedModelElement
  • InheritedNotifier- > InheritedNotifierElement
  • LeafRenderObjectWidget- > LeafRenderObjectElement
  • SingleChildRenderObjectWidget - > SingleChildRenderObjectElement
  • MultiChildRenderObjectWidget- > MultiChildRenderObjectElement
  • ParentDataWidget- > ParentDataElement

这些元素中的每一种都有自己的行为。 例如:


  • StatefulElement将在初始化时调用widget.createState()方法,这将创建一个State并将其与元素关联
  • 装入类型为RenderObjectElement的元素时,它将创建一个RenderObject 。 该renderObject将被添加到Render Tree并与该元素相关联。

步骤2. renderObjects


现在,在完成所有与元素关联的操作之后, 元素树将稳定下来。 因此,现在该考虑可视化过程了。


由于RendererBinding负责渲染Render Tree ,因此WidgetsBinding调用drawFrame RendererBinding方法。


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



在此步骤中,执行以下操作:


  • 请求每个标记为脏的 renderObject进行合成 (即,计算其大小和几何形状)
  • 每个标记为“需要重绘”的renderObject均使用其自己的layer方法重绘
  • 所形成的场景已形成并发送到Flutter Engine ,以便后者将其传输到设备屏幕。
  • 最后,语义也将更新并发送到Flutter Engine

在此工作流程结束时,设备屏幕将刷新。


第3部分:加工手势


使用GestureBinding处理手势( 与玻璃上的手指动作有关的事件 )。


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


  1. 转换Flutter Engine给定的坐标以匹配设备像素比率 ,然后
  2. renderView检索屏幕部分中与事件坐标相关的所有RenderObject 列表
  3. 然后遍历生成的renderObjects列表,并将相关事件发送给每个对象
  4. 如果renderObject “侦听”此类事件,则对其进行处理

希望现在我了解renderObjects的重要性。


第4部分:动画


本文的这一部分是关于动画的概念和对Ticker的深入了解。


在处理动画时,通常使用AnimationController或任何用于动画的小部件( 请注意: AnimatedCrossFade )。


Flutter中,所有与动画相关的内容都指Ticker 。 激活时, 股票代码只有一个任务:“它要求SchedulerBinding注册回调,并在出现新的回调时告诉Flutter Engine唤醒它。” 当发动机颤准备好,它调用通过SchedulerBinding请求:“onBeginFrame”。 SchedulerBinding访问代码回调列表并执行每个列表。


每个刻度都被“感兴趣的”控制器拦截以对其进行处理。 如果动画已完成,则代码将 “禁用”,否则代码将请求SchedulerBinding来计划新的回调。 等等...


全图


现在我们已经了解了Flutter的工作原理:



构建上下文


最后,回到显示不同类型元素的图,并考虑根元素的签名:


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

我们看到了非常著名的BuildContext ! 那是什么


BuildContext是一个接口,它定义了可以由元素实现的许多吸气剂和方法。 通常, BuildContext用于StatelessWidgetbuild()方法或StatefulWidget的 State


BuildContext -这是不一样的元素本身,它对应于
  • 正在更新的小部件(在buildbuilder方法内部)
  • 与在其中引用上下文变量的相关联的StatefulWidget。

这意味着大多数开发人员在不了解元素的情况下就不断使用它们。


BuildContext有多有用?


BuildContext , , , BuildContext , :


  • RenderObject , (, Renderer , -)
  • RenderObject
  • . , of (, MediaQuery.of(context) , Theme.of(context) …)


, , BuildContext, . StatelessWidget , StatefulWidget , setState() , BuildContext .



, !

– , StatelessWidget .
, , StatefulWidget .

 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 , , , , . , , Widget , Element , BuildContext , RenderObject , . , .


. .


PS , () .
PSS Flutter internals Didier Boelens, )

Source: https://habr.com/ru/post/zh-CN476018/


All Articles