JHHK

欢迎来到我的个人网站
行者常至 为者常成

三棵树和渲染原理⭐️⭐️

目录

三棵树

在Flutter渲染的流程中,有三棵重要的树,Widget树,Element树,Render树
Flutter引擎针对Render树进行渲染。

一、Widget树

1、StatelessWidget

主要特点
UI不会动态更新
一旦创建,其属性(final修饰)不可更改,完全依赖外部传入的数据。

生命周期
StatelessWidget的生命周期非常简单:
创建:通过构造函数初始化。
构建:调用build方法生成UI。
销毁:当不再需要时被移除。

使用场景
静态UI:当UI不需要根据数据变化而更新时。
纯展示组件:例如显示文本、图标、图片等。

2、StatefulWidget

主要特点
UI可以动态更新
可以存储状态(数据),当状态变化时触发UI更新

生命周期
StatefulWidget的生命周期较为复杂,主要包括以下阶段:
创建:通过构造函数初始化。
创建State:调用createState方法创建对应的State对象。
初始化:调用initState方法进行初始化。
构建:调用build方法生成UI。
更新:当状态变化时,调用setState触发UI重建。
销毁:调用dispose方法释放资源。

使用场景
动态UI:当UI需要根据数据变化而更新时。
交互组件:例如按钮、输入框、复选框等。

3、Widget树

Widget树是开发者直接编写的UI描述,定义了UI的结构和配置
Widget本身并不直接参与渲染
Widget是轻量级的,创建和销毁成本低。每次build都会重新创建widget树。

补充:
每次build方法被调用时,都会return一个新的Widget实例。
StatelessWidget extends Widget
StatefulWidget extends Widget
RenderObjectWidget extends Widget,直接参与构建 Flutter 的 渲染树(Render Tree),一般不会继承它,而是使用上边两种。当我们需要自定义绘制组件时,比如图表、路径的widget时

二、Element树

1、Element对象

Element对象是widget对象和renderObject对象之间的桥梁
每一个Widget内部都会通过调用createElement方法创建一个Element对象.
这个方法的调用流程是这样的:
widget ——> createElement ——> mount
重要
1) widget对象和element对象是一一对应的,并且element对象会持有这个widget对象。
如果是statefullwidget,还会持有state对象并管理它(创建、初始化、构建、销毁)。
2) widget/state通过回调的方式拿到element对象,而不是拥有element属性,否则循环引用了
3) build方法就是这个回调方法,参数context就是element对象
4) element有几个重要的方法
mount:将Element插入到Element树中。
update:更新Element对应的Widget。
unmount:将Element从Element树中移除。

2、Element树

当Widget树发生变化时,Element树会比较新旧Widget,决定是更新还是重建

比较会调用Widget的静态方法:canUpdate

static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}

当canUpdate返回true时发生了什么?

(1)Widget树
Widget树已经更新为新Widget树。
例如,旧Widget是Text(‘Hello’),新Widget是Text(‘World’)。

(2)Element树
Flutter会复用现有的Element,并调用Element.update方法更新其对应的Widget。
Element会更新其对应的Widget引用。
Element会保留其状态(如State对象)。
重要:返回true时,新widget并不会调用creatState方法,而是复用旧的state,state由element持有,并且state持有widget

例如:
旧Element是TextElement,对应的Widget是Text(‘Hello’)。
新Widget是Text(‘World’),TextElement会更新其Widget引用。

(3)RenderObject树
如果新的Widget属性发生了变化,Flutter会更新对应的RenderObject。
Element会调用RenderObject的更新方法(如RenderObject.markNeedsLayout或RenderObject.markNeedsPaint)。
RenderObject会根据新的属性重新布局或绘制。

例如:
旧RenderObject是RenderParagraph,显示文本’Hello’。
新Widget是Text(‘World’),RenderParagraph会更新文本内容并重新绘制。

当canUpdate返回false时发生了什么?

(1)Widget树
Widget树已经更新为新Widget树。
例如,旧Widget是Text(‘Hello’),新Widget是Container()。

(2)Element树
Flutter会销毁旧的Element及其子树,并创建一个新的Element来匹配新的Widget。
旧Element调用unmount方法,释放资源。
新Element通过newWidget.createElement()创建。

例如:
旧Element是TextElement,新Element是ContainerElement。
TextElement被销毁,ContainerElement被创建。

(3)RenderObject树
旧的RenderObject及其子树会被移除,新的RenderObject会被创建并插入到树中。
旧RenderObject调用dispose方法,释放资源。
新RenderObject通过newElement.createRenderObject()创建。

例如:
旧RenderObject是RenderParagraph,新RenderObject是RenderFlex。
RenderParagraph被销毁,RenderFlex被创建。

补充:
StatelessElement createElement() => StatelessElement(this);
StatefulElement createElement() => StatefulElement(this);
RenderObjectElement createElement();

三、Render树

作用:
RenderObject树负责实际的布局和渲染工作。
每个RenderObject会计算自己的大小、位置,并将内容绘制到屏幕上。 这两个方法是:

// RenderObject类内的两个方法   
// 位于文件:FlutterSDK/flutter_flutter/packages/flutter/lib/src/rendering/object.dart内    
void markNeedsLayout();

void markNeedsPaint();

特点:
RenderObject是重量级的,直接与底层渲染引擎(如Skia)交互。
RenderObject负责处理布局(Layout)、绘制(Paint)和命中测试(Hit Testing)。
RenderObject树是最终决定UI如何显示的核心。

示例:
RenderBox是常见的RenderObject,用于处理矩形区域的布局和渲染。
RenderFlex用于实现Flex布局(如Row和Column)。

结合三棵树来说说CPU和GPU是如何工作的

在Flutter中,屏幕绘制过程涉及CPU和GPU的协同工作。
CPU负责UI的构建和布局计算,而GPU负责最终的渲染和绘制。
下面我们通过一个简单的例子来说明它们各自的工作。

示例场景
假设我们有一个简单的UI,包含一个Container和一个Text,代码如下:

Container(
  color: Colors.blue,
  child: Text('Hello, Flutter!'),
)

CPU的工作

1)构建Widget树
Flutter根据代码生成Widget树:

Container (color: Colors.blue)
└── Text ('Hello, Flutter!')

(2)构建Element树
Flutter根据Widget树创建对应的Element树:

ContainerElement
└── TextElement

(3)构建RenderObject树
Flutter根据Element树创建RenderObject树:

RenderFlex (Container的布局)
└── RenderParagraph (Text的渲染)

(4)布局计算(Layout)
CPU通过RenderObject树计算每个UI元素的大小和位置。
RenderFlex计算Container的布局。
RenderParagraph计算Text的文本布局。

(5)绘制指令生成(Paint)
CPU生成绘制指令,描述如何将UI绘制到屏幕上。
RenderFlex生成绘制蓝色背景的指令。
RenderParagraph生成绘制文本’Hello, Flutter!’的指令。
lxy:就是将计算好的数据交给GPU。

GPU的工作

(1)接收绘制指令
CPU将生成的绘制指令提交给GPU。

(2)光栅化(Rasterization)
GPU将绘制指令转换为实际的像素数据。
将蓝色背景和文本’Hello, Flutter!’转换为屏幕上的像素。

(3)合成(Compositing)
GPU将多个图层合成为最终的屏幕图像。 将蓝色背景和文本图层合成为最终的UI。

(4)显示(Display)
GPU将最终的图像数据发送到帧缓冲区。

总结:
CPU:对象创建、布局计算、绘制指令生成。
GPU:接收绘制指令、坐标变换、光栅化、合成、显示。

以上是通过Flutter Engine 完成的
1.运行dart代码
2.布局:计算位置和大小生成绘制指令-cpu
3.skia绘制:(坐标变换、栅格化),完成绘制-gpu

iOS原生渲染和Flutter渲染有什么不同⭐️

iOS 原生的流程(UIKit + CALayer)

[Object-C 代码]
↓
UIKit 生成 UIView Tree 【CPU】
↓
UIKit 创建并执行布局 (layoutSubviews) & 绘制 (drawRect) → CALayer Tree【CPU】
↓
UIKit(Core Animation) 收集 Layer Tree 快照,准备合成信息【CPU】
↓
UIKit(Core Animation) 将快照提交给系统的 Render Server(backboardd)【CPU】
↓
Render Server 利用 Metal 对Layer Tree进行图层合成 & 光栅化,将图像写入 CAMetalLayer 的 Framebuffer【GPU】
↓
VSync 到来时提交 Framebuffer 到 Display Controller
↓
屏幕刷新

Flutter 的流程(FlutterView + CAMetalLayer)

[Dart 代码]
↓
Flutter Framework 构建 Widget Tree → Element Tree → RenderObject Tree 【CPU】
↓
Flutter Framework 的 RenderObject 执行布局 (layout) & 绘制 (paint) → Layer Tree 【CPU】
↓
Layer Tree 提交给 Flutter Engine(C++)【CPU → GPU】
↓
Flutter Engine 调用 Skia,光栅化 Layer Tree,渲染到 offscreen surface / framebuffer(离屏缓冲)【GPU】
↓
Flutter Engine 收到 VSync 信号将离屏内容提交给 FlutterView 的 CAMetalLayer 的 Framebuffer【GPU】
↓
VSync 到来时提交 Framebuffer 到 Display Controller
↓
屏幕刷新

重要说明

一、关于布局和绘制
布局:在iOS中对应的是layoutSubViews在flutter中是layout,他们的作用都是计算大小和位置。
绘制:在iOS中对应的是drawRect在flutter中是paint,他们的作用都是生成 Layer Tree。

二、关于CAMetalLayer
我们注意到上边的两个流程都有出现CAMetalLayer,CAMetalLayer 是 iOS 和 macOS 平台上用于 Metal 渲染的专用图层(Layer),是 Metal 渲染管线和屏幕之间的桥梁。
你在这上面绘制图像,然后它负责显示到屏幕上。CAMetalLayer 最终会将渲染数据写入 GPU 的帧缓冲区。

三、原生和flutter渲染流程的差异

我们注意到iOS原生的渲染完毕后是将内容写入CAMetalLayer 的 Framebuffer(帧缓冲区),而Flutter的渲染完毕后是将内容写入到一个离屏缓冲区,信号来的时候再写入到CAMetalLayer 的 Framebuffer(帧缓冲区)这是为什么呢?

1、Flutter 是 自己(Skia)在 GPU 上开的一块画布,它独立于系统合成流程。

2、Flutter这块画布自己不知道展示在哪里,要想展示出来,就要写入一个CAMetalLayer。而这个CAMetalLayer就是FlutterViewController.view.layer。

理解了上边几条,我们也就理解了为什么在iOS和Flutter的混合工程中,FlutterViewController在创建的时候会跟FlutterEngine绑定,因为FlutterEngine中skia绘制的画布(离屏缓冲区)最终是要写入到CAMetalLayer才能呈现在屏幕上,而绑定的FlutterViewController.view.layer正是给它提供了一个这样的入口。

我们运行一个iOS和Flutter的混合工程我们会发现:
FlutterViewController绑定的View是FlutterView的实例,而FlutterView的layer实例正是CAMetalLayer。
而iOS原生代码UIViewController绑定的View是UIView实例,UIView的layer实例是CALayer。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.