目录:
- Flutter渲染3颗树和源码分析 (重点)
ComponentElement
RenderObjectElement - Flutter渲染3颗树案例分析
- flutter的屏幕渲染原理Vsync机制
- Flutter渲染3部曲 核心渲染流程
1. Flutter渲染3颗树和源码分析
把视图数据的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。
那么,Widget到底是什么呢?
Widget是Flutter功能的抽象描述,是视图的配置信息,同样也是数据的映射,是Flutter开发框架中最基本的概念。前端框架中常见的名词,比如视图(View)、视图控制器(View Controller)、活动(Activity)、应用(Application)、布局(Layout)等,在Flutter中都是Widget。
事实上,Flutter的核心设计思想便是“一切皆Widget”
1.1 Widget (描述 UI 渲染的配置信息)
Widget是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。
Widget总结: 我们开发最直接的,
绝大多数情况下,我们只需要了解各种Widget特性及使用方法,而无需关心Element及RenderObject!
Widget源码分析:
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
/// * The discussions at [Key] and [GlobalKey].
final Key? key;
@protected
@factory
Element createElement();
static bool canUpdate(Widget oldWidget, Widget newWidget) { // canupdate方法
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
@protected
Widget build(BuildContext context);
那一般我们可以怎么获取布局的大小和位置呢?
Widget大概可以分为三类组合类(紫色标识)、代理类(红色标识)、绘制类(黄色标识)
1). 组合类: Container和Text就是组合类的Widget
2). 代理类: 状态管理的,例如:InheritedWidget用于将一些状态信息传递给子孙Widget。
3). 渲染类: 所有我们在屏幕上看到的UI最终几乎都会通过绘制类的WidgetRenderObjectWidget实现。RenderObjectWidget中有个createRenderObject()方法生成实际渲染的RenderObject对象!
LeafRenderObjectWidget 用于只有渲染功能,无子节点 (如Switch、Radio等)
SingleChildRenderObjectWidget 只含有一个子节点 (如SizedBox、Padding等)
MultiChildRenderObjectWidget 含有多个子节点(如Column、Stack等)
RenderObjectWidget介绍
RenderObjectWidget是一个抽象类。我们通过源码可以看到,这个类中同时拥有创建Element、RenderObject,以及更新RenderObject的方法
RenderObjectWidget本身并不负责这些对象的创建与更新。
1.2 Element (存放上下文,持有 Widget 和 RenderObject)
Element是Widget的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
Element同时持有Widget和RenderObject。而无论是Widget还是Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有RenderObject。那你可能会问,既然都是发号施令,那为什么需要增加中间的这层Element树呢?直接由Widget命令RenderObject去干活儿不好吗?
答案是,可以,但这样做会极大地增加渲染带来的性能损耗。
总结: 中间层, 梳理差异, 提高渲染效率 (diff),
Widget 和 Element 之间是一对多的关系 。
问题:Element是如何创建的?
创建出来后会由framework调用mount方法;
问题: Element会重新创建么?
在 newWidget 与oldWidget的 runtimeType 和 key 相等时会选择使用 newWidget 去更新已经存在的 Element 对象,不然就选择重新创建新的 Element。
Element 持有 RenderObject 和 Widget! Element 是 Widget 和 RenderObject 的粘合剂
Element分为3种, ComponentElement和RenderObjectElement,ProxyElement, 前者负责组合子Element,后者负责渲染, 最后的是代理类
如图:
1). 组合类: ComponentElement主要的子类:
2). 渲染类: RenderObjectElement的主要子类:SingleChildRenderObjectElement、MultiChildRenderObjectElement、RenderObjectToWidgetElement。SingleChildRenderObjectElement
如图: 组合类生成渲染类, 但是渲染类也可以生产出组合类!
而我们也知道 BuildContext 的实现其实是 Element, 所以本质上BuildContext就是当前的Element
abstract class Element extends DiagnosticableTree implements BuildContext {
RenderObject? get renderObject {
Element? current = this;
while (current != null) {
if (current._lifecycleState == _ElementLifecycle.defunct) {
break;
} else if (current is RenderObjectElement) {
return current.renderObject;
} else {
Element? next;
current.visitChildren((Element child) {
assert(next == null); // This verifies that there's only one child.
next = child;
});
current = next;
}
}
return null;
}
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
assert(!_renderObject!.debugDisposed!);
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}
@override
void attachRenderObject(Object? newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null) {
_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);
}
}
1.2.1 组合类ComponentElement的核心流程和核心方法分析:
1.2.2 渲染类RenderObjectElement 的核心流程和核心方法分析:
1.3.1 重点方法Element.inflateWidget() 重点方法
1.3.2重点方法 Element.mount()
1.3.3 重点方法 RenderObjectElement.performRebuild()
1.3.4 RenderObjectElement.update()
1.3.5 RenderObjectElement.updateChildren()
整个创建, 更新的图片如下:
————————————————
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
@override
void performRebuild() {
//更新renderObject
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
1.3 RenderObject (实际渲染树中的对象)
问题: RenderObject是如何创建的?
@override
void mount(Element parent, dynamic newSlot) { // 调用 mount方法!
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this); // 通过widget,创建RenderObject
assert(() {
_debugUpdateRenderObjectOwner();
returntrue;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}
如果你去看类似于Text这种组合类的Widget,它也会执行mount方法,但是mount方法中并没有调用createRenderObject这样的方法。
RenderObject才是真正负责绘制的对象,其中包含了paint,layout等方法~
布局和绘制在RenderObject中完成,Skia: 负责合成和渲染!
更新和变化的流程: canupdate方法
如果Widget的配置数据发生了改变,那么持有该Widget的Element节点也会被标记为dirty。
在下一个周期的绘制时,Flutter就会触发Element树的更新,并使用最新的Widget数据更新自身以及关联的RenderObject对象,
接下来便会进入Layout和Paint的流程。而真正的绘制和布局过程,则完全交由RenderObject完成:
布局和绘制完成后,接下来的事情就交给Skia了。在VSync信号同步时直接从渲染树合成Bitmap,然后提交给GPU
@protected
@pragma('vm:prefer-inline')
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) {
if (child != null) {
deactivateChild(child);
}
return null;
}
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
if (isTimelineTracked) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (kDebugMode && debugEnhanceBuildTimelineArguments) {
debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments();
}
return true;
}());
FlutterTimeline.startSync(
'${newWidget.runtimeType}',
arguments: debugTimelineArguments,
);
}
child.update(newWidget);
if (isTimelineTracked) {
FlutterTimeline.finishSync();
}
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
} else {
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot);
}
assert(() {
if (child != null) {
_debugRemoveGlobalKeyReservation(child);
}
final Key? key = newWidget.key;
if (key is GlobalKey) {
assert(owner != null);
owner!._debugReserveGlobalKeyFor(this, newChild, key);
}
return true;
}());
return newChild;
}
abstract class Widget extends DiagnosticableTree {
final Key? key;
@protected
@factory
Element createElement();
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}
canupdate()方法:
canUpdate(...)是一个静态方法,它主要用于在 widget 树重新build时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidget与oldWidget的runtimeType和key同时相等时就会用new widget去更新Element对象的配置,否则就会创建新的Element。
Key: 这个key属性类似于 React/Vue 中的key,主要的作用是决定是否在下一次build时复用旧的 widget ,决定的条件在canUpdate()方法中
布局,从RenderBox开始,对RenderObject Tree从上至下进行布局。
abstract class RenderBox extends RenderObject {
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! BoxParentData) {
child.parentData = BoxParentData();
}
}
2. Flutter渲染3颗树案例分析
从一个demo分析出Widget,Element 和 RenderObject 3者的关系:
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: Column(
children: <Widget>[
SizedBox(height: 300,width: 100,),
Text("dd"),
Text("aa"),
Text("bb"),
],
),);
}
2.1代码结构对应的widget
组合widget: 蓝色
渲染widget: 黄色
class Container extends StatelessWidget {
Widget build(BuildContext context) {
Widget? current = child;
if (decoration != null) {
current = DecoratedBox(decoration: decoration!, child: current);
}
2.2 Element:
2.3RenderObject :
举例: container是如何对应RenderDecoratedBox!
Container具体的渲染widget是DecoratedBox! DecoratedBox里面创建RenderObject
class Container extends StatelessWidget {
Widget build(BuildContext context) {
current = DecoratedBox(decoration: decoration!, child: current);
}
class DecoratedBox extends SingleChildRenderObjectWidget {
@override
RenderDecoratedBox createRenderObject(BuildContext context) {
return RenderDecoratedBox(
decoration: decoration,
position: position,
configuration: createLocalImageConfiguration(context),
);
}
}
完整的图片过程: 写的widet--->实际widget--->Element----->RenderObject
3颗树总结: 三者的关系:
Flutter渲染过程,可以分为这么三步:
首先,通过Widget树生成对应的Element树;
然后,创建相应的RenderObject并关联到Element.renderObject属性上;
最后,构建成RenderObject树,以完成最终的渲染。
可以大致总结出三者的关系是:配置文件 Widget 生成了 Element,
而后创建 RenderObject 关联到 Element 的内部 renderObject 对象上,
最后Flutter 通过 RenderObject 数据来布局和绘制。
理论上你也可以认为 RenderObject 是最终给 Flutter 的渲染数据,它保存了大小和位置等信息,Flutter 通过它去绘制出画面。
Widget的渲染原理
所有的Widget都会创建一个或者多个Element对象
并不是所有的Widget都会被独立渲染!只有继承RenderObjectWidget的才会创建RenderObject对象!(Container就不会创建RenderObject、column和padding这些可以创建RenderObject)
在Flutter渲染的流程中,有三颗重要的树!Flutter引擎是针对Render树进行渲染!
Widget树、Element树、Render树
每一个Widget都会创建一个Element对象
隐式调用createElement方法。Element加入Element树中,它会创建RenderElement、ComponentElement(又分为StatefulElement和StatelessElement)。
RenderElement主要是创建RenderObject对象, 继承RenderObjectWidget的Widget会创建RenderElement
创建RanderElement
Flutter会调用mount方法,调用createRanderObject方法
StatefulElement继承ComponentElement,StatefulWidget会创建StatefulElement
调用createState方法,创建State
将Widget赋值给state
调用state的build方法 并且将自己(Element)传出去,build里面的context 就是Widget的Element !
StatelessElement继承ComponentElement,StatelessWidget会创建StatelessElement
mount方法 -> firstBuild -> rebuild -> performBuild -> build -> _widget.build
-主要就是调用build方法 并且将自己(Element)传出去
问题:
1. 已知widget, 如何找到对应的renderObject?
先找到对应的渲染的widget, 然后在里面找renderObject!
renderObject创建是在渲染的widget里面创建的, 而不是Element创建的!
2. Widget会重新创建么?Element会重新创建么?RenderObject 会重新创建么?
Widget 重新创建,Element 树和 RenderObject 树并不会完全重新创建。
Element什么时候创建?
在每一次创建Widget的时候,会创建一个对应的Element,然后将该元素插入树中
3. flutter的屏幕渲染原理Vsync机制
在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。
CPU 把计算好的、需要显示的内容交给 GPU,由 GPU 完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。
屏幕渲染原理:
问题: 原生渲染和flutter渲染的共同点和差异是啥?
底层是用skia, 其他封装调用不一样!
问题: Skia是什么?
要想了解Flutter,你必须先了解它的底层图像渲染引擎Skia。
因为,Flutter只关心如何向GPU提供视图数据,而Skia就是它向GPU提供视图数据的好帮手。
Skia是一款用C++开发的、性能彪悍的2D图像绘制引擎,其前身是一个向量绘图软件。2005年被Google公司收购后,因为其出色的绘制表现被广泛应用在Chrome和Android等核心产品上。Skia在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的API。
因此,架构于Skia之上的Flutter,也因此拥有了彻底的跨平台渲染能力。通过与Skia的深度定制及优化,Flutter可以最大限度地抹平平台差异,提高渲染效率与性能。
底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。也就是说,Skia保证了同一套代码调用在Android和iOS平台上的渲染效果是完全一致的。
问题: flutter新引擎impller与skia的区别
字节跳动: 深入解析Flutter下一代渲染引擎Impeller (ok)
https://juejin.cn/user/3368492743792695/posts
渲染的源码分析:
请求 Vsync 信号
SchedulerBinding:
mixin SchedulerBinding on BindingBase {
@override
void initInstances() {
super.initInstances();
_instance = this;
if (!kReleaseMode) {
addTimingsCallback((List<FrameTiming> timings) {
timings.forEach(_profileFramePostEvent);
});
}
}
/// dart
/// 调用 C++ 到 Native 层,请求 Vsync 信号
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) {
return;
}
assert(() {
if (debugPrintScheduleFrameStacks) {
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
}
return true;
}());
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
void ensureFrameCallbacksRegistered() {
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
void scheduleFrame() native 'Window_scheduleFrame'; // c++方法
void _handleBeginFrame(Duration rawTimeStamp) {
if (_warmUpFrame) {
// "begin frame" and "draw frame" must strictly alternate. Therefore
// _rescheduleAfterWarmUpFrame cannot possibly be true here as it is
// reset by _handleDrawFrame.
assert(!_rescheduleAfterWarmUpFrame);
_rescheduleAfterWarmUpFrame = true;
return;
}
handleBeginFrame(rawTimeStamp);
}
void _handleDrawFrame() {
if (_rescheduleAfterWarmUpFrame) {
_rescheduleAfterWarmUpFrame = false;
// Reschedule in a post-frame callback to allow the draw-frame phase of
// the warm-up frame to finish.
addPostFrameCallback((Duration timeStamp) {
// Force an engine frame.
//
// We need to reset _hasScheduledFrame here because we cancelled the
// original engine frame, and therefore did not run handleBeginFrame
// who is responsible for resetting it. So if a frame callback set this
// to true in the "begin frame" part of the warm-up frame, it will
// still be true here and cause us to skip scheduling an engine frame.
_hasScheduledFrame = false;
scheduleFrame();
});
return;
}
handleDrawFrame();
}
以安卓为例,最终会执行到 JNI_OnLoad 注册的 Java 接口 AsyncWaitForVsyncDelegate.asyncWaitForVsync,这个接口在 Flutter 启动时初始化。实现内容如下
Flutter引擎启动时,向系统的Choreographer实例注册接收Vsync的回调函数,GPU硬件发出Vsync后,系统会触发该回调函数,并驱动UI线程进行layout和绘制。
new FlutterJNI.AsyncWaitForVsyncDelegate() {
@Override
public void asyncWaitForVsync(long cookie) {
Choreographer.getInstance()
.postFrameCallback(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
float fps = windowManager.getDefaultDisplay().getRefreshRate();
long refreshPeriodNanos = (long) (1000000000.0 / fps);
FlutterJNI.nativeOnVsync(
frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
});
}
}
Choreographer.getInstance().postFrameCallback 用于监听系统垂直同步信号,在下一个垂直信号来临时回调 doFrame,通过 FlutterJNI.nativeOnVsync 走到 c++ 中。经过复杂的链路,将下面的任务添加到到 UI Task Runner 中的事件队列中:
lib/ui/window/platform_configuration.cc
void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime) {
.................
// 调用 dart 中的 _window.onBeginFrame
tonic::LogIfError(
tonic::DartInvoke(begin_frame_.Get(), {Dart_NewInteger(microseconds),}));
// 执行 microTask
UIDartState::Current()->FlushMicrotasksNow();
// 调用 dart 中的 _window.onDrawFrame
tonic::LogIfError(tonic::DartInvokeVoid(draw_frame_.Get()));
}
当接收到Vsync时,会调用到RendererBinding的drawFrame方法;
[源码路径:flutter/lib/src/rendering/binding.dart]
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 触发RenderObject执行布局
pipelineOwner.flushCompositingBits(); //绘制之前的预处理操作
pipelineOwner.flushPaint(); // 触发RenderObject执行绘制
renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
pipelineOwner.flushSemantics(); // 发送语义化给系统,用于辅助功能等
}
Window 正是 Flutter Framework 连接宿主操作系统的接口
class Window {
// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;
// 当前系统默认的语言Locale
Locale get locale;
// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;
// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
runApp这个方法总的来说就是做了以下的事情
在Flutter的framework层和engine层建立一个连接WidgetsFlutterBinding,注册Vsync回调后,每一帧调用的时候都会触发WidgetsFlutterBinding里面的方法,从而去调用framework层的处理逻辑
为传入的widget构建节点树,将节点树中的RenderObjct树的结果交给enginee层的SingletonFlutterWindow,然后通知到GPU进行渲染
总结:
1)、scheduleFrame 回调 C++ 注册 Vsync 信号
2)、Vsnc 来临,回调 C++ 调用 window.onBeginFrame,这个方法映射到 dart 中的 handleBeginFrame(), window.onDrawFrame
对应 dart 中的 drawFrame()
3)、drawFrame 中,build 阶段根据 widget 生成 element 和 renderObject 树;layout 测量绘制元素的大小和位置;paint 阶段生成 layer 树;
4). 然后Framework层通过Window的render方法回到了Engine层,Engine再向GPU线程提交绘制任务;最终渲染出来
dart请求绘制---->jni----->dart
总结:整个流程由 Dart 发起,通过 C++ 回调到 Native 层,注册一次垂直同步信号的监听。等到信号来到,再通知 Dart 进行渲染。可以看出,Flutter 上的渲染,是先由 Dart 侧主动发起,而不是被动等待垂直信号的通知。这可以解释,比如一些静态页面时,整个屏幕不会多次渲染。并且由于是 Native 层的垂直同步信号,所以也完全适配高刷的设备
上面整个流程下来就是每一次Vsync信号,在Dart层所做的处理。
核心就是执行drawFrame方法。而drawFrame方法主要有三个步骤:
drawFrame() 总结:
1).遍历需要layout的RenderObject,让它执行performLayout方法;整个调用栈是:RenderView.performLayout->child.layout->child.performLayout->child.layout->…;
2). 遍历需要paint的RenderObject,让它执行paint方法;整个调用栈是:RenderView.paint->PaintingContext.paintChild->child.paint->PaintingContext.paintChild->…;
3). 通过PaintingContext.canvas可以把RenderObject绘制的内容绘制到PaintingContext._currentLayer上,最终构造出Scene实例,通过Window.render方法把Scene发送给Engine层,最终由Engine将内容渲染在设备屏幕上。
————————————————
渲染管道7个步骤:
flutter完整的渲染管道涉及到很多步骤:
1). 准备前3个子步骤: 首先得到用户的输入,例如触摸屏幕事件,一些动画可能会随之产生,然后开始构建组件并去渲染它们;
Build:还记得一开始 setState() 将 element 加入了脏集合么?这个阶段,Flutter 会通过 widget 更新所有脏集合中的节点(需要更新)中的 element 与 RenderObject 树。
2). 渲染可以细分为3个子步骤;
2.1. Layout(布局),它的作用是在屏幕上确定每个组件的大小和位置;
2.2. Paint(绘制),它提供一系列方法把组件渲染成用户看到的样子;
2.3. Composite(图层合成)它把绘制步骤生成的图层或者纹理堆叠在一起,按照顺序组织它们,以便它们可以高效的在屏幕上进行呈现,图层合成是组件最终呈现在屏幕上之前很关键的也是最后的一个优化步骤;
3). 最后是光栅化,它把抽象的表现映射成物理像素显示在屏幕上。Paint 阶段会触发 RenderObject 对象绘制,生成第四棵树:Layer Tree,最终合成光栅化后完成渲染。
合成
所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率
渲染
合并完成后,Flutter会将几何图层数据交由Skia引擎加工成二维图像数据,最终交由GPU进行渲染,完成界面的展示
Build:还记得一开始 setState() 将 element 加入了脏集合么
Layout:RenderObject 树进行布局测量,用于确定每一个展示元素的大小和位置。
Paint:Paint 阶段会触发 RenderObject 对象绘制,生成第四棵树:Layer Tree,最终合成光栅化后完成渲染。
最详细的: dart-->native ----> 屏幕显示的完整过程
1). Flutter 引擎启动时,向系统的 Choreographer 实例注册接收 Vsync 的回调。
2). 平台发出 Vsync 信号后,上一步注册的回调被调用,一系列调用后,执行到 VsyncWaiter::fireCallback。
- .VsyncWaiter::fireCallback实际上会执行Animator类的成员函数BeginFrame。
4). BeginFrame 经过一系列调用执行到 Window 的 BeginFrame,Window 实例是连接底层 Engine 和 Dart framework 的重要桥梁,基本上所以跟平台相关的操作都会由 Window 实例来串联,包括事件,渲染,无障碍等。
5). 通过 Window 的 BeginFrame 调用到 Dart Framework的RenderBinding 类,其有一个方法叫 drawFrame ,这个方法会去驱动 UI 上的 dirty 节点进行重排和绘制,如果遇到图片的显示,会丢到 IO 线程以及去 worker 线程去执行图片加载和解码,解码完成后,再次丢到 IO 线程去生成图片纹理,由于 IO 线程和 GPU 线程是 share GL context 的,所以在 IO 线程生成的图片纹理在 GPU 线程可以直接被 GPU 所处理和显示。
6). Dart 层绘制所产生的绘制指令以及相关的渲染属性配置都会存储在 LayerTree 中,通过 Animator::RenderFrame 把 LayerTree 提交到 GPU 线程,GPU 线程拿到 LayerTree 后,进行光栅化并做上屏操作(关于LayerTree我们后面会详细讲解)。之后通过 Animator::RequestFrame 请求接收系统下一次的Vsync信号,这样又会从第1步开始,循环往复,驱动 UI 界面不断的更新。
4.渲染三部曲:
RenderObject 绘制3部曲
4.1 Layout:
measure : 没有看到测量, 这个和layout在一起
Flutter采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程
void layout(Constraints constraints, { bool parentUsesSize = false }) {
assert(!_debugDisposed);
if (!kReleaseMode && debugProfileLayoutsEnabled) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (debugEnhanceLayoutTimelineArguments) {
debugTimelineArguments = toDiagnosticsNode().toTimelineArguments();
}
return true;
}());
FlutterTimeline.startSync(
'$runtimeType',
arguments: debugTimelineArguments,
);
}
assert(constraints.debugAssertIsValid(
isAppliedConstraint: true,
informationCollector: () {
final List<String> stack = StackTrace.current.toString().split('\n');
int? targetFrame;
final Pattern layoutFramePattern = RegExp(r'^#[0-9]+ +Render(?:Object|Box).layout \(');
for (int i = 0; i < stack.length; i += 1) {
if (layoutFramePattern.matchAsPrefix(stack[i]) != null) {
targetFrame = i + 1;
} else if (targetFrame != null) {
break;
}
}
if (targetFrame != null && targetFrame < stack.length) {
final Pattern targetFramePattern = RegExp(r'^#[0-9]+ +(.+)$');
final Match? targetFrameMatch = targetFramePattern.matchAsPrefix(stack[targetFrame]);
final String? problemFunction = (targetFrameMatch != null && targetFrameMatch.groupCount > 0) ? targetFrameMatch.group(1) : stack[targetFrame].trim();
return <DiagnosticsNode>[
ErrorDescription(
"These invalid constraints were provided to $runtimeType's layout() "
'function by the following function, which probably computed the '
'invalid constraints in question:\n'
' $problemFunction',
),
];
}
return <DiagnosticsNode>[];
},
));
assert(!_debugDoingThisResize);
assert(!_debugDoingThisLayout);
final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
final RenderObject relayoutBoundary = isRelayoutBoundary ? this : parent!._relayoutBoundary!;
assert(() {
_debugCanParentUseSize = parentUsesSize;
return true;
}());
if (!_needsLayout && constraints == _constraints) {
assert(() {
// in case parentUsesSize changed since the last invocation, set size
// to itself, so it has the right internal debug values.
_debugDoingThisResize = sizedByParent;
_debugDoingThisLayout = !sizedByParent;
final RenderObject? debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
debugResetSize();
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugDoingThisResize = false;
return true;
}());
if (relayoutBoundary != _relayoutBoundary) {
_relayoutBoundary = relayoutBoundary;
visitChildren(_propagateRelayoutBoundaryToChild);
}
if (!kReleaseMode && debugProfileLayoutsEnabled) {
FlutterTimeline.finishSync();
}
return;
}
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
// The local relayout boundary has changed, must notify children in case
// they also need updating. Otherwise, they will be confused about what
// their actual relayout boundary is later.
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
assert(!_debugMutationsLocked);
assert(!_doingThisLayoutWithCallback);
assert(() {
_debugMutationsLocked = true;
if (debugPrintLayouts) {
debugPrint('Laying out (${sizedByParent ? "with separate resize" : "with resize allowed"}) $this');
}
return true;
}());
if (sizedByParent) {
assert(() {
_debugDoingThisResize = true;
return true;
}());
try {
performResize();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_reportException('performResize', e, stack);
}
assert(() {
_debugDoingThisResize = false;
return true;
}());
}
RenderObject? debugPreviousActiveLayout;
assert(() {
_debugDoingThisLayout = true;
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
return true;
}());
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_reportException('performLayout', e, stack);
}
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugMutationsLocked = false;
return true;
}());
_needsLayout = false;
markNeedsPaint();
if (!kReleaseMode && debugProfileLayoutsEnabled) {
FlutterTimeline.finishSync();
}
}
4.2 绘制paint:
void paint(PaintingContext context, Offset offset) { }
布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter会把所有的渲染对象绘制到不同的图层上。
与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。
Flutter提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘
重绘边界的一个典型场景是Scrollview。ScrollView滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了