水果网站策划书,网站的营销方法有哪些,网站工程师招聘,阜新旅游网站建设Widget、Element、BuildContext 和 RenderObject
Widget
Widget关键类及其子类继承关系如图所示#xff1a; 其中#xff0c;Widget是Widget Tree所有节点的基类。Widget的子类主要分为3类#xff1a; 第1类是RenderObjectWidget的子类#xff0c;具体来说又分为SingleCh…Widget、Element、BuildContext 和 RenderObject
Widget
Widget关键类及其子类继承关系如图所示 其中Widget是Widget Tree所有节点的基类。Widget的子类主要分为3类 第1类是RenderObjectWidget的子类具体来说又分为SingleChildRenderObjectWidget单子节点容器、LeafRenderObjectWidget叶子节点、MultiChildRenderObjectWidget多子节点容器它们的共同特点是都对应了一个RenderObject 的子类可以进行Layout、Paint等逻辑。 第2类是StatelessWidget和StatefulWidget它们是开发者最常用的Widget自身不具备绘制能力即不对应Render Object但是可以组织和配置RenderObjectWidget类型的Widget。 第3类是ProxyWidget具体来说又分为ParentDataWidget和InheritedWidget它们的特点是为其子节点提供额外的数据。
Element
Element的关键类及其子类继承关系如图所示 从图5-2中可以清楚的看到Element的继承关系它实现了BuildContext接口图5-2与图5-1相对应每一个Element都有一个对应的Widget。Element有两个直接的子类 ComponentElement 和 RenderObjectElement其中 ComponentElement 的两个子类 StatelessElement 和 StatefulElement 就分别对应了 StatelessWidget 和 StatefulWidget 。
我们知道最终的UI树其实是由一个个独立的Element节点构成。组件最终的Layout、渲染都是通过RenderObject来完成的从创建到渲染的大体流程是根据Widget生成Element然后创建相应的RenderObject并关联到Element.renderObject属性上最后再通过RenderObject来完成布局排列和绘制。
Element就是Widget在UI树具体位置的一个实例化对象大多数Element只有唯一的renderObject但还有一些Element会有多个子节点如继承自RenderObjectElement的一些类比如MultiChildRenderObjectElement。最终所有Element的RenderObject构成一棵树我们称之为”Render Tree“即”渲染树“。
总结一下我们可以认为Flutter的UI系统包含三棵树Widget树、Element树、渲染树。他们的依赖关系是Element树根据Widget树生成而渲染树又依赖于Element树如图所示。 现在我们重点看一下ElementElement的生命周期如下 Framework 调用Widget.createElement 创建一个Element实例记为element Framework 调用 element.mount(parentElement,newSlot) mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置这一步不是必须的一般发生在Element树结构发生变化时才需要重新添加。插入到渲染树后的element就处于“active”状态处于“active”状态后就可以显示在屏幕上了可以隐藏。 当有父Widget的配置数据改变时同时其State.build返回的Widget结构与之前不同此时就需要重新构建对应的Element树。为了进行Element复用在Element重新构建前会先尝试是否可以复用旧树上相同位置的elementelement节点在更新前都会调用其对应Widget的canUpdate方法如果返回true则复用旧Element旧的Element会使用新Widget配置数据更新反之则会创建一个新的Element。 Widget.canUpdate主要是判断newWidget与oldWidget的runtimeType和key是否同时相等如果同时相等就返回true否则就会返回false。根据这个原理当我们需要强制更新一个Widget时可以通过指定不同的Key来避免复用。 当有祖先Element决定要移除element 时如Widget树结构发生了变化导致element对应的Widget被移除这时该祖先Element就会调用deactivateChild 方法来移除它移除后element.renderObject也会被从渲染树中移除然后Framework会调用element.deactivate 方法这时element状态变为“inactive”状态。 “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element“inactive”态的element在当前动画最后一帧结束前都会保留如果在动画执行结束后它还未能重新变成“active”状态Framework就会调用其unmount方法将其彻底移除这时element的状态为defunct,它将永远不会再被插入到树中。 如果element要重新插入到Element树的其他位置如element或element的祖先拥有一个GlobalKey用于全局复用元素那么Framework会先将element从现有位置移除然后再调用其activate方法并将其renderObject重新attach到渲染树。 总结
一个Element对象将在被创建时初始化initial状态并在通过mount方法加入Element Tree后变为active状态当该节点对应的Widget失效后其自身会通过deactivate方法进入inactive状态。如果在当前帧的Build过程中有其他Element节点通过key复用了该节点则会通过activate方法使得该节点再次进入active状态如果当前帧结束后该节点仍不在Element Tree中则会通过unmount方法进行卸载并进入defunct状态等待后续逻辑的销毁。
看完Element的生命周期可能有些人会有疑问开发者会直接操作Element树吗
其实对于开发者来说大多数情况下只需要关注Widget树就行Flutter框架已经将对Widget树的操作映射到了Element树上这可以极大的降低复杂度提高开发效率。
但是了解Element对理解整个Flutter UI框架是至关重要的Flutter正是通过Element这个纽带将Widget和RenderObject关联起来了解Element层不仅会帮助开发者对Flutter UI框架有个清晰的认识而且也会提高自己的抽象能力和设计能力。另外在有些时候我们必须得直接使用Element对象来完成一些操作比如获取主题Theme数据。
BuildContext
我们已经知道StatelessWidget和StatefulWidget的build方法都会传一个BuildContext对象
Widget build(BuildContext context) {}我们也知道在很多时候我们都需要使用这个context 做一些事比如
Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject那么BuildContext到底是什么呢查看其定义发现其是一个抽象接口类
abstract class BuildContext {...
}那这个context对象对应的实现类到底是谁呢我们顺藤摸瓜发现build调用是发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build方法中例如在StatelessElement中
class StatelessElement extends ComponentElement {...overrideWidget build() widget.build(this);...
}同样在StatefulElement 中
class StatefulElement extends ComponentElement {... overrideWidget build() state.build(this);...
}发现build传递的参数是this很明显这个BuildContext就是StatelessElement或StatefulElement本身。但StatelessElement和StatefulElement本身并没有实现BuildContext接口继续跟踪代码发现它们间接继承自Element类然后查看Element类定义发现Element类果然实现了BuildContext接口:
abstract class ComponentElement extends Element {...}
abstract class Element extends DiagnosticableTree implements BuildContext {...}至此真相大白BuildContext就是widget对应的Element所以我们可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。我们获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。
总结BuildContext 就是 Element本尊通过 BuildContext 的方法调用就是在操作 ElementWidget 是外衣而 Element就是外衣下的裸体。
BuildContext 的另一层含义
关于 BuildContext 的另一层含义就是它是对Widget在Widget树中的位置的引用它包含了关于Widget在Widget树中的位置的信息而不是关于Widget本身的信息。
以主题为例由于每个Widget都有自己的BuildContext 这意味着如果你将多个主题分散在树中那么获取一个Widget的主题可能会返回与另一个Widget不同的结果。在计数器应用示例程序中的主题特定情况下或在其他of方法中你将会获取到树中距离最近的该类型的父节点。 进阶
我们可以看到Element是Flutter UI框架内部连接widget和RenderObject的纽带大多数时候开发者只需要关注widget层即可但是widget层有时候并不能完全屏蔽Element细节所以Framework在StatelessWidget和StatefulWidget中通过build方法参数又将Element对象也传递给了开发者这样一来开发者便可以在需要时直接操作Element对象。
那么现在有两个问题
1. 如果没有 widget 层单靠 Element 层是否可以搭建起一个可用的UI框架如果可以应该是什么样子 2. Flutter UI 框架能不做成响应式吗
对于问题 1答案当然是肯定的因为我们之前说过widget树只是Element树的映射它只提供描述UI树的配置信息Widget 就是外衣一个人不穿衣服当然也可以比较羞耻地活着但是穿上衣服他会活的更体面即便不依赖Widget 我们也可以完全通过Element来搭建一个UI框架。
下面举一个例子
我们通过纯粹的Element来模拟一个StatefulWidget的功能假设有一个页面该页面有一个按钮按钮的文本是一个9位数点击一次按钮则对9个数随机排一次序代码如下
class HomeView extends ComponentElement{HomeView(Widget widget) : super(widget);String text 123456789;overrideWidget build() {Color primary Theme.of(this).primaryColor; //1return GestureDetector(child: Center(child: TextButton(child: Text(text, style: TextStyle(color: primary),),onPressed: () {var t text.split()..shuffle();text t.join();markNeedsBuild(); //点击后将该Element标记为dirtyElement将会rebuild},),),);}
}上面build方法不接收参数这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用this代替即可如代码注释 1 处Theme.of(this)参数直接传this即可因为当前对象本身就是Element实例。 当text发生改变时我们调用markNeedsBuild()方法将当前Element标记为dirty即可标记为dirty的Element会在下一帧中重建。实际上State.setState()在内部也是调用的markNeedsBuild()方法。 上面代码中build方法返回的仍然是一个widget这是由于Flutter框架中已经有了widget这一层并且组件库都已经是以widget的形式提供了如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供那么就可以用纯Element来构建UI了。HomeView的build方法返回值类型就可以是Element了。
如果我们需要将上面代码在现有Flutter框架中跑起来那么还是得提供一个“适配器”widget将HomeView结合到现有框架中下面CustomHome就相当于“适配器”
class CustomHome extends Widget {overrideElement createElement() {return HomeView(this);}
}现在就可以将CustomHome添加到widget树了我们在一个新路由页创建它最终效果如下图所示 点击按钮则按钮文本会随机排序。
对于问题 2答案当然也是肯定的Flutter 引擎提供的 API 是原始且独立的这个与操作系统提供的API类似上层UI框架设计成什么样完全取决于设计者完全可以将UI框架设计成 Android 风格或 iOS 风格但这些事Google不会再去做。所以在理论上我们可以做但是没必要这是因为响应式的思想本身是很棒的之所以提出这个问题是因为做与不做是一回事但知道能不能做是另一回事这能反映出我们对知识的理解程度。
RenderObject
我们说过每个Element都对应一个RenderObject我们可以通过Element.renderObject 来获取。并且我们也说过RenderObject的主要职责是Layout和绘制所有的RenderObject会组成一棵渲染树Render Tree。下面将重点介绍一下RenderObject的作用。
RenderObject就是渲染树中的一个对象它主要的作用是实现事件响应以及渲染管线中除过 build 的执行过程build 过程由 element 实现即包括布局、绘制、层合成以及上屏。 RenderObject关键类及其子类如图5-3所示其每个子类都对应了一个RenderObjectWidget 类型的Widget节点。
RenderView是一个特殊的RenderObject是整个Render Tree的根节点。另外一个特殊的RenderObject是RenderAbstractViewport它是一个抽象类。RenderViewport会实现其接口并间接继承自RenderBox。RenderBox和RenderSliver是Flutter中最常见的RenderObjectRenderBox负责行、列等常规布局而 RenderSliver 负责列表内每个Item的布局。
RenderObject拥有一个parent和一个parentData 属性parent指向渲染树中自己的父节点而parentData是一个预留变量在父组件的布局过程会确定其所有子组件布局信息如位置信息即相对于父组件的偏移而这些布局信息需要在布局阶段保存起来因为布局信息在后续的绘制阶段还需要被使用用于确定组件的绘制位置而parentData属性的主要作用就是保存布局信息比如在 Stack 布局中RenderStack就会将子元素的偏移数据存储在子元素的parentData中具体可以查看Positioned实现。
问题既然有了RenderObjectFlutter框架为什么还要专门提供RenderBox和 RenderSliver两个子类 这是因为RenderObject类本身实现了一套基础的布局和绘制协议但是却并没有定义子节点模型如一个节点可以有几个子节点 它也没有定义坐标系统如子节点定位是在笛卡尔坐标中还是极坐标和具体的布局协议是通过宽高还是通过constraint和size?或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等。 为此Flutter框架提供了一个RenderBox和一个 RenderSliver类它们都是继承自RenderObject布局坐标系统采用笛卡尔坐标系屏幕的(top, left)是原点。而 Flutter 基于这两个类分别实现了基于 RenderBox 的盒模型布局和基于 Sliver 的按需加载模型。
启动流程根节点构建流程
Flutter Engine 是基于Dart运行环境即 Dart RuntimeDart Runtime 的启动关键流程如下 其中Dart Runtime 会首先创建和启动 DartVM虚拟机而 DartVM启动后则会初始化一个DartIsolate然后启动它在DartIsolate启动流程的最后就会执行Dart应用程序的入口main方法。也就是我们日常开发中 lib/main.dart的main()函数
void main() runApp(MyApp());可以看main()函数只调用了一个runApp()方法我们看看runApp()方法中都做了什么
void runApp(Widget app) {final WidgetsBinding binding WidgetsFlutterBinding.ensureInitialized(); binding..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))..scheduleWarmUpFrame();
}这里参数app是一个 widget它就是我们开发者传给Flutter框架的Widget是 Flutter 应用启动后要展示的第一个组件而WidgetsFlutterBinding正是绑定widget 框架和Flutter 引擎的桥梁定义如下
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {static WidgetsBinding ensureInitialized() {if (WidgetsBinding._instance null) {WidgetsFlutterBinding();}return WidgetsBinding.instance;}
}先看一下 WidgetsFlutterBinding 的继承关系我们发现WidgetsFlutterBinding继承自BindingBase 并混入了很多Binding类所以其启动时将按照mixin的顺序依次触发这些类的构造函数。 GestureBinding负责手势的处理提供了window.onPointerDataPacket 回调绑定Framework手势子系统是Framework事件模型与底层事件的绑定入口。ServicesBinding负责提供平台相关能力提供了window.onPlatformMessage 回调 用于绑定平台消息通道message channel主要处理原生和Flutter通信。SchedulerBinding负责渲染流程中各种回调的管理提供了window.onBeginFrame和window.onDrawFrame回调监听刷新事件绑定Framework绘制调度子系统。PaintingBinding负责绘制相关的逻辑绑定绘制库主要用于处理图片缓存。SemanticsBinding负责提供无障碍能力语义化层与Flutter engine的桥梁主要是辅助功能的底层支持。RendererBinding: 负责Render Tree的最终渲染持有PipelineOwner对象提供了window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与Flutter engine的桥梁。WidgetsBinding负责 Flutter 3 棵树的管理持有BuilderOwner对象提供了window.onLocaleChanged、onBuildScheduled 等回调。它是Flutter widget层与engine的桥梁。
在了解为什么要混入这些Binding之前我们先介绍一下WindowWindow 是 Flutter Framework 连接宿主操作系统的接口。我们看一下 Window类的部分定义
class Window { // 当前设备的DPI即一个逻辑像素显示多少物理像素数字越大显示效果就越精细保真。// DPI是设备屏幕的固件属性如Nexus 6的屏幕DPI为3.5 double get devicePixelRatio _devicePixelRatio; // Flutter UI绘制区域的大小Size get physicalSize _physicalSize; // 当前系统默认的语言LocaleLocale get locale; // 当前系统字体缩放比例。 double get textScaleFactor _textScaleFactor; // 当绘制区域大小改变回调VoidCallback get onMetricsChanged _onMetricsChanged; // Locale发生变化回调VoidCallback get onLocaleChanged _onLocaleChanged;// 系统字体缩放变化回调VoidCallback get onTextScaleFactorChanged _onTextScaleFactorChanged;// 绘制前回调一般会受显示器的垂直同步信号VSync驱动当屏幕刷新时就会被调用FrameCallback get onBeginFrame _onBeginFrame;// 绘制回调 VoidCallback get onDrawFrame _onDrawFrame;// 点击或指针事件回调PointerDataPacketCallback get onPointerDataPacket _onPointerDataPacket;// 调度Frame该方法执行后onBeginFrame和onDrawFrame将紧接着会在合适时机被调用// 此方法会直接调用Flutter engine的Window_scheduleFrame方法void scheduleFrame() native Window_scheduleFrame;// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法void render(Scene scene) native Window_render; // 发送平台消息void sendPlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) ;// 平台通道消息处理回调 PlatformMessageCallback get onPlatformMessage _onPlatformMessage; ... //其他属性及回调
}可以看到Window类包含了当前设备和系统的一些信息以及Flutter Engine的一些回调。
现在我们再回来看看WidgetsFlutterBinding混入的各种Binding。通过查看这些 Binding的源码我们可以发现这些Binding中基本都是监听并处理Window对象的一些事件然后将这些事件按照Framework的模型包装、抽象然后分发。可以看到WidgetsFlutterBinding正是粘连 Flutter Engine 与上层Framework 的“胶水”。WidgetsFlutterBinding的本质就是一个WidgetsBinding自身并没有特殊逻辑所以通过混入这些binding类获得了额外的能力。
而WidgetsFlutterBinding.ensureInitialized()方法中主要负责初始化了一个WidgetsBinding的全局单例并返回WidgetsBinding单例对象除此外没有做任何其他事情。这也正说明了它只是一个站在众人肩膀上的粘合剂。
再回到runApp方法中获得WidgetsBinding单例对象后紧接着会调用WidgetsBinding的scheduleAttachRootWidget方法而在其中又调用了attachRootWidget方法代码如下
void scheduleAttachRootWidget(Widget rootWidget) { Timer.run(() { attachRootWidget(rootWidget); }); // 注意不是立即执行
}
void attachRootWidget(Widget rootWidget) {final bool isBootstrapFrame rootElement null;_readyToProduceFrames true; // 开始生成 Element Tree_rootElement RenderObjectToWidgetAdapterRenderBox(container: renderView, // Render Tree的根节点debugShortDescription: [root],child: rootWidget, // 开发者通过runApp传入Widget Tree的根节点).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElementRenderBox?);if (isBootstrapFrame) {SchedulerBinding.instance.ensureVisualUpdate(); // 请求渲染 }
}以上逻辑正是驱动Element Tree和Render Tree进行创建的入口需要注意的是attachRootWidget是通过 Timer.run启动的这是为了保证所有逻辑都处于消息循环的管理中。
attachRootWidget方法主要负责将根Widget添加到RenderView上注意代码中有renderView和renderViewElement两个变量renderView是一个RenderObject它是渲染树的根而renderViewElement是renderView对应的Element对象可见该方法主要完成了根widget到根 RenderObject再到根Element的整个关联过程。
attachToRenderTree方法将驱动Element Tree的构建并返回其根节点 源码实现如下
RenderObjectToWidgetElementT attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElementT? element ]) {if (element null) { // 首帧构建element参数为空owner.lockState(() {element createElement(); // 创建Widget对应的Elementelement!.assignOwner(owner); // 绑定BuildOwner});owner.buildScope(element!, () { // 开始子节点的解析与挂载 element!.mount(null, null); }); } else { // 如热重载等场景element._newWidget this;element.markNeedsBuild();}return element!;
}该方法负责创建根element即RenderObjectToWidgetElement并且将element与widget 进行关联即创建出 widget树对应的element树。如果 element 已经创建过了则将根element 中关联的widget 设为新的由此可以看出element 只会创建一次后面会进行复用。由于首帧的element参数为null因此首先通过createElement方法完成创建然后和BuildOwner的实例绑定那么BuildOwner是什么呢其实它就是widget framework的管理类它跟踪哪些 widget 需要重新构建。该对象将在后面驱动Element Tree的更新。
在完成3棵树的构建之后会触发attachRootWidget中的ensureVisualUpdate的逻辑
void ensureVisualUpdate() {switch (schedulerPhase) {case SchedulerPhase.idle: // 闲置阶段没有需要渲染的帧// 计算注册到本次帧渲染的一次性高优先级回调通常是与动画相关的计算case SchedulerPhase.postFrameCallbacks:scheduleFrame(); return;case SchedulerPhase.transientCallbacks: // 处理Dart中的微任务// 计算待渲染帧的数据包括Build、Layout、Paint等流程这部分内容后面将详细介绍case SchedulerPhase.midFrameMicrotasks:// 帧渲染的逻辑结束处理注册到本次帧渲染的一次性低优先级回调case SchedulerPhase.persistentCallbacks:return;}
}以上逻辑将根据当前所处的阶段判断是否需要发起一次帧渲染每个阶段的状态转换如图5-8所示。
在图5-8中首先如果没有外部如setState方法和内部如动画心跳、图片加载完成的监听器的驱动Framework将默认处于idle状态。如果有新的帧数据请求渲染Framework将在Engine的驱动下在handleBeginFrame方法中进入transientCallbacks状态主要是处理高优先级的一次性回调比如动画计算。完成以上逻辑后Framework会将自身状态更新为midFrameMicrotasks具体的微任务处理由Engine驱动。其次Engine会调用handleDrawFrame方法Framework在此时将状态更新为persistentCallbacks表示自身将处理每帧必须执行的逻辑主要是与渲染管道相关的内容。完成Framework中与渲染管道相关的逻辑后Framework会将自身状态更新为postFrameCallbacks并处理低优先级的一次性回调通常是由开发者或者上层逻辑注册。最后Framework将状态重置为idle。idle是Framework的最终状态只有在需要帧渲染时才会开始一次状态循环。
scheduleFrame方法的逻辑如下所示它将通过platformDispatcher.scheduleFrame接口发起请求要求在下一个Vsync信号到达的时候进行渲染。
void scheduleFrame() {if (_hasScheduledFrame || !framesEnabled) return;ensureFrameCallbacksRegistered(); platformDispatcher.scheduleFrame();_hasScheduledFrame true;
}回到runApp的实现中在组件树在构建build完毕后当调用完attachRootWidget后最后一步会调用 WidgetsFlutterBinding 实例的 scheduleWarmUpFrame() 方法该方法的实现在SchedulerBinding 中它被调用后会立即进行一次绘制在此次绘制结束前该方法会锁定事件分发也就是说在本次绘制结束完成之前 Flutter 将不会响应各种事件这可以保证在绘制过程中不会再触发新的重绘。scheduleWarmUpFrame方法的代码如下
// flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleWarmUpFrame() { if (_warmUpFrame || schedulerPhase ! SchedulerPhase.idle) return; // 已发送帧渲染请求_warmUpFrame true;Timeline.startSync(Warm-up frame);final bool hadScheduledFrame _hasScheduledFrame;Timer.run(() { // 第1步动画等相关逻辑handleBeginFrame(null); });Timer.run(() { // 第2步立即渲染一帧通常是首帧handleDrawFrame();resetEpoch();_warmUpFrame false; // 首帧渲染完成if (hadScheduledFrame) scheduleFrame();});lockEvents(() async { // 第3步首帧渲染前不消费手势await endOfFrame;Timeline.finishSync();});
}以上逻辑主要分为3步但需要注意的是第3步是最先执行的因为前两步是在Timer.run方法中启动的。handleBeginFrame方法将触发动画相关的逻辑handleDrawFrame方法将触发3棵树的更新以及Render Tree的Layout和Paint等渲染逻辑。正常来说这两个逻辑是Engine通过监听Vsync信号驱动的这里之所以直接执行是为了保证首帧尽快渲染因为不管Vsync信号何时到来首帧都是必须渲染的。
总结 渲染管线
前面分析了runApp方法在执行完ensureInitialized方法所触发的初始化流程后将触发scheduleAttachRootWidget和scheduleWarmUpFrame两个方法前者负责Render Tree的生成后者负责首帧渲染的触发。
1. Frame
一次绘制过程我们称其为一帧frame。我们之前说的 Flutter 可以实现60fpsFrame Per-Second就是指一秒钟最多可以触发 60 次重绘FPS 值越大界面就越流畅。这里需要说明的是 Flutter中 的 frame 概念并不等同于屏幕刷新帧frame因为Flutter UI 框架的 frame 并不是每次屏幕刷新都会触发这是因为如果 UI 在一段时间不变那么每次屏幕刷新都重新走一遍渲染流程是不必要的因此Flutter 在第一帧渲染结束后会采取一种主动请求 frame 的方式来实现只有当UI可能会改变时才会重新走渲染流程。
Flutter 在 window 上注册一个 onBeginFrame和一个 onDrawFrame 回调在onDrawFrame 回调中最终会调用 drawFrame。当我们调用 window.scheduleFrame() 方法之后Flutter引擎会在合适的时机可以认为是在屏幕下一次刷新之前具体取决于Flutter引擎的实现来调用onBeginFrame和onDrawFrame。
可见只有主动调用scheduleFrame()才会执行 drawFrame。所以我们在Flutter 中的提到 frame 时如无特别说明则是和 drawFrame() 的调用对应而不是和屏幕的刷新频率对应。
2. Flutter 调度过程 SchedulerPhase
Flutter 应用执行过程简单来讲分为 idle 和 frame 两种状态idle 状态代表没有 frame 处理如果应用状态改变需要刷新 UI则需要通过scheduleFrame()去请求新的 frame当 frame 到来时就进入了frame状态整个Flutter应用生命周期就是在 idle 和 frame 两种状态间切换。
frame 处理流程
当有新的 frame 到来时具体处理过程就是依次执行四个任务队列transientCallbacks、midFrameMicrotasks、persistentCallbacks、postFrameCallbacks当四个任务队列执行完毕后当前 frame 结束。综上Flutter 将整个生命周期分为五种状态通过 SchedulerPhase 枚举类来表示它们
enum SchedulerPhase {/// 空闲状态并没有 frame 在处理。这种状态代表页面未发生变化并不需要重新渲染。/// 如果页面发生变化需要调用scheduleFrame()来请求 frame。/// 注意空闲状态只是指没有 frame 在处理通常微任务、定时器回调或者用户事件回调都/// 可能被执行比如监听了tap事件用户点击后我们 onTap 回调就是在idle阶段被执行的。idle,/// 执行”临时“回调任务”临时“回调任务只能被执行一次执行后会被移出”临时“任务队列。/// 典型的代表就是动画回调会在该阶段执行。transientCallbacks,/// 在执行临时任务时可能会产生一些新的微任务比如在执行第一个临时任务时创建了一个/// Future且这个 Future 在所有临时任务执行完毕前就已经 resolve 了这中情况/// Future 的回调将在[midFrameMicrotasks]阶段执行midFrameMicrotasks,/// 执行一些持久的任务每一个frame都要执行的任务比如渲染管线构建、布局、绘制/// 就是在该任务队列中执行的.persistentCallbacks,/// 在当前 frame 在结束之前将会执行 postFrameCallbacks通常进行一些清理工作和/// 请求新的 frame。postFrameCallbacks,
}3. 渲染管线rendering pipeline
当新的 frame 到来时调用到 WidgetsBinding 的 drawFrame() 方法我们来看看它的实现
override
void drawFrame() {...//省略无关代码try {buildOwner.buildScope(renderViewElement); // 先执行构建super.drawFrame(); //然后调用父类的 drawFrame 方法}
}实际上关键的代码就两行先重新构建build然后再调用父类的 drawFrame 方法我们将父类的 drawFrame方法展开后
void drawFrame() {buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树//下面是 展开 super.drawFrame() 方法pipelineOwner.flushLayout(); // 2.更新布局pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息pipelineOwner.flushPaint(); // 4.重绘if (sendFramesToEngine) {renderView.compositeFrame(); // 5. 上屏会将绘制出的bit数据发送给GPU...}
}可以看到主要做了5件事
重新构建widget树。更新布局。更新“层合成”信息。重绘。上屏将绘制的产物显示在屏幕上。
我们称上面的5步为 rendering pipeline中文翻译为 “渲染流水线” 或 “渲染管线”。
任何一个UI框架无论是Web还是Android都会有自己的渲染管道渲染管道是UI框架的核心负责处理用户的输入、生成UI描述、栅格化绘制指令、上屏最终数据等。Flutter也不例外。由于采用了自渲染的方式Flutter的渲染管道是独立于平台的。以Android为例Flutter只是通过Embedder获取了一个Surface或者Texture作为自己渲染管道的最终输出目标。 Flutter的渲染管道需要要通过来自系统Vsync信号的驱动当需要更新UI的时候Framework会通知EngineEngine会等到下个Vsync信号到达的时候会通知Framework进行animate, buildlayoutpaint最后生成 layer 提交给Engine。Engine会把 layer 进行组合生成纹理最后通过Open GL接口提交数据给GPU GPU经过处理后在显示器上面显示如下图 具体来说Flutter的渲染管道分为以下7个步骤。 1用户输入User Input响应用户通过鼠标、键盘、触摸屏等设备产生的手势行为。 2动画Animation基于计时器Timer更新当前帧的数据。 3构建Build三棵树的创建、更新与销毁阶段StatelessWidget 和 State 的build方法将在该阶段执行。 4布局LayoutRender Tree将在这个阶段完成每个节点的大小和位置的计算。 5绘制Paint Render Tree 遍历每个节点生成 Layer Tree RenderObject的paint方法将在该阶段执行生成一系列绘制指令。 6合成Composition处理 Layer Tree生成一个Scene对象作为栅格化 的输入。 7栅格化Rasterize将绘制指令处理为可供GPU上屏的原始数据。
下面我们以 setState 的执行更新的流程为例先对整个更新流程有一个大概的了解。
setState 执行流程
当 setState 调用后
首先调用当前 element 的 markNeedsBuild 方法将当前 element 的_dirty标记为 true 。接着调用 scheduleBuildFor将当前 element 添加到BuildOwner的 _dirtyElements 列表中。同时会请求一个新的 frame随后会绘制新的 frameonBuildScheduled-ensureVisualUpdate-scheduleFrame() 。
下面是 setState 执行的大概流程图 其中 updateChild() 的逻辑如下 其中 onBuildScheduled 方法在启动阶段完成初始化它最终将调用ensureVisualUpdate它将触发 Vsync 信号的监听。当新的 Vsync 信号到达后将触发 buildScope 方法这会进行重建子树同时会执行渲染管线流程
void drawFrame() {buildOwner!.buildScope(renderViewElement!); //重新构建widget树pipelineOwner.flushLayout(); // 更新布局pipelineOwner.flushCompositingBits(); //更新合成信息pipelineOwner.flushPaint(); // 更新绘制if (sendFramesToEngine) {renderView.compositeFrame(); // 上屏会将绘制出的bit数据发送给GPUpipelineOwner.flushSemantics(); // this also sends the semantics to the OS._firstFrameSent true;}
}重新构建 widget 树如果 dirtyElements 列表不为空则遍历该列表调用每一个element的rebuild方法重新构建新的widget树由于新的widget(树)使用新的状态构建所以可能导致widget布局信息占用的空间和位置发生变化如果发生变化则会调用其renderObject的markNeedsLayout方法该方法会从当前节点向父级查找直到找到一个relayoutBoundary的节点然后会将它添加到一个全局的nodesNeedingLayout列表中如果直到根节点也没有找到relayoutBoundary则将根节点添加到nodesNeedingLayout列表中。 更新布局遍历nodesNeedingLayout数组对每一个renderObject重新布局调用其layout方法确定新的大小和偏移。layout方法中会调用markNeedsPaint()该方法和 markNeedsLayout 方法功能类似也会从当前节点向父级查找直到找到一个isRepaintBoundary属性为true的父节点然后将它添加到一个全局的nodesNeedingPaint列表中由于根节点RenderView的 isRepaintBoundary 为 true所以必会找到一个。查找过程结束后会调用 buildOwner.requestVisualUpdate 方法该方法最终会调用scheduleFrame()该方法中会先判断是否已经请求过新的frame如果没有则请求一个新的frame。 更新合成信息先忽略。 更新绘制遍历nodesNeedingPaint列表调用每一个节点的paint方法进行重绘绘制过程会生成Layer。需要说明一下flutter中绘制结果是保存在Layer中的也就是说只要Layer不释放那么绘制的结果就会被缓存因此Layer可以跨frame来缓存绘制结果避免不必要的重绘开销。Flutter框架绘制过程中遇到isRepaintBoundary 为 true 的节点时才会生成一个新的Layer。可见Layer和 renderObject 不是一一对应关系父子节点可以共享这个我们会在随后的一个试验中来验证。当然如果是自定义组件我们可以在renderObject中手动添加任意多个 Layer这通常用于只需一次绘制而随后不会发生变化的绘制元素的缓存场景这个随后我们也会通过一个例子来演示。 上屏绘制完成后我们得到的是一棵Layer树最后我们需要将Layer树中的绘制信息在屏幕上显示。我们知道Flutter是自实现的渲染引擎因此我们需要将绘制信息提交给Flutter engine而renderView.compositeFrame 正是完成了这个使命。
以上便是setState调用到UI更新的大概更新过程实际的流程会更复杂一些比如在build过程中是不允许再调用setState的框架需要做一些检查。又比如在frame中会涉及到动画的的调度、在上屏时会将所有的Layer添加到场景Scene对象后再渲染Scene。
setState 执行时机问题
setState 会触发 build而 build 是在执行 persistentCallbacks 阶段执行的因此只要不是在该阶段执行 setState 就绝对安全但是这样的粒度太粗比如在transientCallbacks 和 midFrameMicrotasks 阶段如果应用状态发生变化最好的方式是只将组件标记为 dirty而不用再去请求新的 frame 因为当前frame 还没有执行到 persistentCallbacks因此后面执行到后就会在当前帧渲染管线中刷新UI。因此setState 在标记完 dirty 后会先判断一下调度状态如果是 idle 或 执行 postFrameCallbacks 阶段才会去请求新的 frame :
void ensureVisualUpdate() {switch (schedulerPhase) {case SchedulerPhase.idle:case SchedulerPhase.postFrameCallbacks:scheduleFrame(); // 请求新的framereturn;case SchedulerPhase.transientCallbacks:case SchedulerPhase.midFrameMicrotasks:case SchedulerPhase.persistentCallbacks: // 注意这一行return;}
}上面的代码在大多数情况下是没有问题的但是如果我们在 build 阶段又调用 setState 的话还是会有问题因为如果我们在 build 阶段又调用 setState 的话就又会导致 build…这样将导致循环调用因此 flutter 框架发现在 build 阶段调用 setState 的话就会报错如 overrideWidget build(BuildContext context) {return LayoutBuilder(builder: (context, c) {// build 阶段不能调用 setState, 会报错setState(() {index;});return Text(xx);},);}运行后会报错控制台会打印 Exception caught by widgets library
The following assertion was thrown building LayoutBuilder:
setState() or markNeedsBuild() called during build.需要注意如果我们直接在 build 中调用setState 代码如下
override
Widget build(BuildContext context) {setState(() {index;});return Text($index);
} 运行后是不会报错的原因是在执行 build 时当前组件的 dirty 状态对应的element中为 true只有 build 执行完后才会被置为 false。而 setState 执行的时候会会先判断当前 dirty 值如果为 true 则会直接返回因此就不会报错。
上面我们只讨论了在 build 阶段调用 setState 会导致错误实际上在整个构建、布局和绘制阶段都不能同步调用 setState这是因为在这些阶段调用 setState 都有可能请求新的 frame都可能会导致循环调用因此如果要在这些阶段更新应用状态时都不能直接调用 setState。
安全更新
现在我们知道在 build 阶段不能调用 setState了实际上在组件的布局阶段和绘制阶段也都不能直接再同步请求重新布局或重绘道理是相同的那在这些阶段正确的更新方式是什么呢我们以 setState 为例可以通过如下方式
// 在build、布局、绘制阶段安全更新
void update(VoidCallback fn) {SchedulerBinding.instance.addPostFrameCallback((_) {setState(fn);});
}注意update 函数只应该在 frame 执行 persistentCallbacks 时执行其他阶段直接调用 setState 即可。因为 idle 状态会是一个特例如果 在idle 状态调用 update 的话需要手动调用 scheduleFrame() 请求新的 frame否则 postFrameCallbacks 在下一个frame 其他组件请求的 frame 到来之前不会被执行因此我们可以将 update 修改一下
void update(VoidCallback fn) {final schedulerPhase SchedulerBinding.instance.schedulerPhase;if (schedulerPhase SchedulerPhase.persistentCallbacks) {SchedulerBinding.instance.addPostFrameCallback((_) {setState(fn);});} else {setState(fn);}
}至此我们封装了一个可以安全更新状态的 update 函数。
现在我们回想一下“自定义组件CustomCheckbox” 一节中为了执行动画我们在绘制完成之后通过如下代码请求重绘 SchedulerBinding.instance.addPostFrameCallback((_) {...markNeedsPaint();});我们并没有直接调用 markNeedsPaint()而原因正如上面所述。
总结 需要说明的是 Build 过程和 Layout 过程是可以交替执行的。 参考
《Flutter实战·第二版》《Flutter内核源码剖析》