Flutter 笔记 | Flutter 核心原理(二)关键类和启动流程
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树,如图所示。
现在我们重点看一下Element
,Element
的生命周期如下:
-
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
重新构建前会先尝试是否可以复用旧树上相同位置的element
,element
节点在更新前都会调用其对应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 {...Widget build() => widget.build(this);...
}
同样在StatefulElement
中:
class StatefulElement extends ComponentElement {... Widget 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
的方法调用就是在操作 Element
,Widget
是外衣,而 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";Widget 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标记为dirty,Element将会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 {Element 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中最常见的RenderObject
,RenderBox
负责行、列等常规布局,而RenderSliver
负责列表内每个Item
的布局。
RenderObject
拥有一个parent
和一个parentData
属性,parent
指向渲染树中自己的父节点,而parentData
是一个预留变量,在父组件的布局过程,会确定其所有子组件布局信息(如位置信息,即相对于父组件的偏移),而这些布局信息需要在布局阶段保存起来,因为布局信息在后续的绘制阶段还需要被使用(用于确定组件的绘制位置),而parentData
属性的主要作用就是保存布局信息,比如在 Stack
布局中,RenderStack
就会将子元素的偏移数据存储在子元素的parentData
中(具体可以查看Positioned
实现)。
问题:既然有了RenderObject
,Flutter框架为什么还要专门提供RenderBox
和 RenderSliver
两个子类?
-
这是因为
RenderObject
类本身实现了一套基础的布局和绘制协议,但是却并没有定义子节点模型(如一个节点可以有几个子节点?), 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。 -
为此,Flutter框架提供了一个
RenderBox
和一个RenderSliver
类,它们都是继承自RenderObject
,布局坐标系统采用笛卡尔坐标系,屏幕的(top, left)
是原点。而 Flutter 基于这两个类分别实现了基于RenderBox
的盒模型布局和基于Sliver
的按需加载模型。
启动流程(根节点构建流程)
Flutter Engine 是基于Dart运行环境,即 Dart Runtime,Dart 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
之前我们先介绍一下Window
,Window
是 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 = RenderObjectToWidgetAdapter<RenderBox>(container: renderView, // Render Tree的根节点debugShortDescription: '[root]',child: rootWidget, // 开发者通过runApp传入Widget Tree的根节点).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);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
的构建,并返回其根节点, 源码实现如下:
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? 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 可以实现60fps(Frame 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()
方法,我们来看看它的实现:
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会通知Engine,Engine会等到下个Vsync
信号到达的时候,会通知Framework进行animate, build,layout,paint,最后生成 layer
提交给Engine。Engine会把 layer
进行组合,生成纹理,最后通过Open GL接口提交数据给GPU, GPU经过处理后在显示器上面显示,如下图:
具体来说,Flutter的渲染管道分为以下7个步骤。
-
(1)用户输入(User Input):响应用户通过鼠标、键盘、触摸屏等设备产生的手势行为。
-
(2)动画(Animation):基于计时器(Timer)更新当前帧的数据。
-
(3)构建(Build):三棵树的创建、更新与销毁阶段,
StatelessWidget
和State
的build
方法将在该阶段执行。 -
(4)布局(Layout):
Render 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
,随后会绘制新的frame
:onBuildScheduled->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
的话就会报错,如:
Widget 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
,代码如下:
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内核源码剖析》
相关文章:

Flutter 笔记 | Flutter 核心原理(二)关键类和启动流程
Widget、Element、BuildContext 和 RenderObject Widget Widget关键类及其子类继承关系如图所示: 其中,Widget是Widget Tree所有节点的基类。Widget的子类主要分为3类: 第1类是RenderObjectWidget的子类,具体来说又分为SingleCh…...

Android:主题切换
一.概述 正在开发的应用做了一版新UI,原打算将新版UI按项目名做成资源包,再在build.gradle里productFlavors{ }多渠道打包实现 但被告知新旧两个项目共用一个分支,那就做成两个主题(Theme1/Theme2)来适配了 如果只是变更UI,做成…...

terminalworks ASP.NET Core PDF 浏览器-Crack
ASP.NET Core 的 PDF 查看器 terminalworks在 ASP.NET Core 网页或应用程序中添加可靠的 PDF 查看器的简单方法。 我们的 Web PDF 查看器基于经过验证和测试的 Mozilla PdfJS 解决方案,该解决方案在 Firefox 中用作默认 PDF 查看器。我们专门设计了我们的查看器&…...

Rust每日一练(Leetday0020) 最后单词的长度、螺旋矩阵II、排列序列
目录 58. 最后一个单词的长度 Length of Last Word 🌟 59. 螺旋矩阵 II Spiral Matrix II 🌟🌟 60. 排列序列 Permutation Sequence 🌟🌟🌟 🌟 每日一练刷题专栏 🌟 Rust每日…...

短视频矩阵源码如何做应用编程?
短视频矩阵源码, 短视频矩阵系统技术文档: 可以采用电子文档或者纸质文档的形式交付,具体取决于需求方的要求。电子文档可以通过电子邮件、远程指导交付云存储等方式进行传输、 短视频矩阵{seo}源码是指将抖音平台上的视频资源进行筛选、排…...

【运维知识进阶篇】Ansible实现一套完整LNMP架构
前面介绍了PlayBook怎么写服务部署,把服务部署上后,我们来用Ansible来部署项目,实现一套完整的LNMP架构。我们部署wordpress、wecenter、phpshe、phpmyadmin这四个项目。将其所有的剧本都写入lnmp.yml中,相关备份数据都放入root/a…...

Spring Boot 自动配置一篇概览
一、什么是自动配置 bean 自动配置类通过添加 AutoConfiguration 注解实现。 因为 AutoConfiguration 注解本身是以 Configuration 注解的,所以自动配置类可以算是一个标准的基于 Configuration 注解的类。 Conditional 注解可以用于声明自动配置启用条件&#x…...

深入理解设计原则之接口隔离原则(ISP)【软件架构设计】
系列文章目录 C高性能优化编程系列 深入理解软件架构设计系列 深入理解设计模式系列 高级C并发线程编程 LSP:接口隔离原则 系列文章目录1、接口隔离原则的定义和解读2、案例解读3、如何判断一个接口是否符合接口隔离原则?小结 1、接口隔离原则的定义和…...

IMX6ULL裸机篇之I2C实验主控代码说明二
一. I2C实验 I2C实验内容: 学习如何使用 I.MX6U 的 I2C 接口来驱动 AP3216C,读取 AP3216C 的传感器数据。 I2C读写数据时序图: I2C写数据时序图如下: I2C读数据时序图如下: 二. I2C主控读写时序 1. 读数据与写数…...

【计算机组成原理与体系结构】数据的表示与运算
目录 一、进位计数制 二、信息编码 三、定点数数据表示 四、校验码 五、定点数补码加减运算 六、标志位的生成 七、定点数的移位运算 八、定点数的乘除运算 九、浮点数的表示 十、浮点数的运算 一、进位计数制 整数部分: 二进制、八进制、十六进制 --…...

如何入门编程
随着信息技术的快速发展,编程已经成为一个越来越重要的技能。那么,我们该如何入门编程呢?欢迎大家积极讨论 一、自学编程需要注意什么? 对于我个人的理解,其实自学编程最重要的就是兴趣。你得培养编程兴趣。 所以在学…...

SQL中CONVERT转化日期函数的使用方法
SQL中CONVERT转化日期函数的使用方法 SQL中CONVERT函数最常用的是使用convert转化长日期为短日期,如果只要取yyyy-mm-dd格式时间, 就可以用convert(nvarchar(10),field,120) 120 是格式代码, nvarchar(10) 是指取出前10位字符. 例如 SELECT CONVERT(nvarchar(10),…...

SpringBoot2-核心技术(一)
SpringBoot2-核心技术(一) 了解SpringBoot配置文件的使用 文章目录 SpringBoot2-核心技术(一)了解SpringBoot配置文件的使用一、文件类型1. properties2. yaml 二、yaml的基本使用1. 基本语法2. 数据类型2.1 字面量 2.2 对象2.3 …...

mac host学习
参考: SSH中known_hosts文件作用和常见问题及解决方法 https://blog.csdn.net/luduoyuan/article/details/130070120在 Mac 上更改 DNS 设置 https://support.apple.com/zh-cn/guide/mac-help/mh14127/mac mac中有时候你输入的域名,但会跳转到与期望ip不…...

Java之~指定String日期时间,5分钟一截取时间
// 截取5分钟时间Testpublic void timeCutForDay() throws ParseException {String startTime "2023-03-28 09:16:03";String endTime "2023-03-31 23:59:59";SimpleDateFormat dateFormat new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");D…...

【chatGPT4结对编程】chatGPT4教我做图像分类
开始接触深度学习 大语言模型火了之后,我也想过是否要加入深度学习的行业当中来,一开始的想法就是AI大模型肯定会被各大厂垄断,我们作为普通应用型软件工程师直接调用api就完事,另外对自己的学历也自卑(刚刚够线的二本࿰…...

Different romantic
001 他暗恋上我们班上的一个女生。 He has a crush on a girl in our class. crush n. 迷恋 have a crush on (someone) 暗恋(某人) crush 也可以指“暗恋的对象”。例如,“他在大学曾经暗恋过两个人”,英语就是He had two crushe…...

learn C++ NO.7——C/C++内存管理
引言 现在是5月30日的正午,图书馆里空空的,也许是大家都在午休,也许是现在37摄氏度的气温。穿着球衣的我已经汗流浃背,今天热火战胜了凯尔特人,闯入决赛。以下克上的勇气也激励着我,在省内垫底的大学中&am…...

SDUT数据库原理——第十章作业(参考答案)
1. 简述使用检查点方法进行数据恢复的一般步骤。 答: (1)使用检查点方法进行数据恢复,首先从重新开始文件(见P302页图10.3)中找到最后一个检查点记录在日志文件中的地址,由该地址在日志文件中找到最后一个检查点记录。 (2)由该检查点记录得到检查点建立时刻所有正在…...

My Note of Diffusion Models
Diffusion Models Links: https://theaisummer.com/diffusion-models/ Markovian Hierachical VAE rvs: data: x 0 x_{0} x0,representation: x T x_{T} xT ( p ( x 0 , x 1 , ⋯ , x T ) , q ( x 1 , ⋯ , x T ∣ x 0 ) ) (p(x_0,x_1,\cdots,x_T),q(x_1,\cdots,x_{T…...

【P37】JMeter 仅一次控制器(Once Only Controller)
文章目录 一、仅一次控制器(Once Only Controller)参数说明二、测试计划设计2.1、测试计划一2.1、测试计划二 一、仅一次控制器(Once Only Controller)参数说明 可以让控制器内部的逻辑只执行一次;单次的范围是针对某…...

cleanmymac要不要下载装机?好不好用
当我们收到一台崭新的mac电脑,第一步肯定是找到一款帮助我们管理电脑运行的“电脑管家”,监控内存运行、智能清理系统垃圾、清理Mac大文件旧文件、消除恶意软件、快速卸载更新软件、隐私保护、监控系统运行状况等。基本在上mac电脑防护一款CleanMyMac就够…...

DNS风险分析及防护研究(五):常见的DNS威胁与防御(中科三方)
DNS是互联网运行重要的基础设施,在全球互联网运转中扮演重要作用。互联网中的每一次访问都开始于一次DNS查询,从而将人们更好辨识的域名转换为数字化的IP地址。随着互联网的快速发展以及网络技术的快速发展,DNS固有的缺陷逐步暴露出来&#x…...

使用geoserver发布shp和tiff数据
一、安装并启动geoserver服务 1.1 下载geoserver 进入官网下载 由于geoserver是使用Java语言开发的,所以运行需要java的环境,不同geoserver的版本号对java的版本要求不同,所以选择版本时需注意对应java的版本要求,由于我本地安…...

谷歌周彦祺:LLM浪潮中的女性科学家多面手丨智源大会嘉宾风采
导读 大模型研发竞赛如火如荼,谷歌紧随OpenAI其后推出PalM2、Gemini等系列模型。Scaling Law是否仍然适用于当下的大模型发展?科技巨头与初创企业在竞争中各有哪些优势和劣势?模型研究者应秉持哪些社会责任? 2023智源大会「基础模…...

Burp模块
Target模块 记录流量 1.Target按主机或域名分类记录 2.HTTP History 按时间顺序记录且会记录很多次 3.Target模块的作用 (1)把握网站的整体情况 (2)对一次工作的域进行分析 (3)分析网站存在的攻击面 …...

sql笔记:SQL SERVER字符串填充(标量值函数创建、标量值函数调用)
/*字符串填充 ,如果返回 -1 说明输入参数有错误*/ CREATE FUNCTION [dbo].[uf_pad_string] ( @string_unpadded VARCHAR(100), --123填充前字符串 @pad_char VARCHAR(1), --0 填充的字符串 @pad_count tinyint, --10 填充后字符串长度 @pad_p…...

python使用hTTP方法
Python中可以使用requests库来发送HTTP请求,其中包括GET、POST、PUT、DELETE等方法。下面是一个使用requests库发送HTTP请求的示例: python import requests # 发送GET请求 response requests.get(Example Domain) # 发送POST请求 data {key1: valu…...

JavaSE常用API
1. Math.round(11.5)等于多少?Math.round(- 11.5) 又等于多少? Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5然后进行取整。 2. switch 是否能作用在 byte 上,是否能作用在 long 上…...

华为OD机试之模拟商场优惠打折(Java源码)
模拟商场优惠打折 题目描述 模拟商场优惠打折,有三种优惠券可以用,满减券、打折券和无门槛券。 满减券:满100减10,满200减20,满300减30,满400减40,以此类推不限制使用; 打折券&…...