JHHK

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

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

目录

Widget介绍

一、Widget的种类

按继承关系分类

维度 StatelessWidget StatefulWidget RenderObjectWidget ProxyWidget
是否有状态 ❌ 无 ✅ 有(State 对象) ❌ 自身无状态 ❌ 自身无状态
状态存放位置 State 中 RenderObject 中 不存状态
是否直接创建 RenderObject
是否参与布局 / 绘制 间接 间接 ✅ 直接参与 ❌ 不参与
主要职责 描述静态 UI
配置文件/数据管理者
描述可变 UI
配置文件/数据管理者
布局 + 绘制 包裹 / 转发
rebuild 触发方式 父组件 rebuild setState() / 父组件    
rebuild 是否创建新对象 创建新 Widget 创建新 Widget 更新 RenderObject 不创建 RenderObject
Element 类型 StatelessElement StatefulElement RenderObjectElement ProxyElement
典型使用场景 静态UI、展示型组件(不需要交互) 交互UI、动态变化组件    

自身不变化使用StatelessWidget, 自身变化使用StatefulWidget

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

StatefulWidget的生命周期较为复杂,主要包括以下阶段:

Widget的构造方法
Widget的CreateState方法
State的构造方法
State的initState方法
State的didChangeDependencies方法:init之后调用。依赖的InheritedWidget发生变化之后,方法也会调用!
State的build方法:当调用setState的方法,会重新调用build进行渲染
State的deactivate方法:当state对象从渲染树种移除的时候调用,即将销毁
State的dispose方法:当Widget销毁的时候

RenderObjectWidget 负责真正的布局、绘制、hitTest

二、为什么 Text 继承 StatelessWidget?

首先解释为什么是StatelessWidget?
因为文本只需要展示,不需要状态,不需要自己更新显示,所以它是StatelessWidget。

然后解释为什么不是RenderObjectWidget?
Text 明明能显示文字,为什么它不是 RenderObjectWidget?文字到底是谁画的?

Text继承StatelessWidge,所以Text也是一个容器或者说数据管理者,它存放了各种配置数据,不会被渲染。
但Text这个容器内包含一个RichText,而 RichText 是继承 RenderObjectWidget

class Text extends StatelessWidget {
  const Text( this.data, {this.style,this.textAlign,
    ...
  });

  final String data;
  final TextStyle? style;

  @override
  Widget build(BuildContext context) {
    return RichText(
      text: TextSpan(
        text: data,
        style: style,
      ),
    );
  }
}

(Text 包含 RichText extends RenderObjectWidget) -> RenderObjectElement -> RenderParagraph
所有的文字最终都是通过RenderParagraph绘制的

三、为什么 Image extends StatefulWidget?

首先解释为什么是StatefulWidget?
图片未加载、已加载、加载失败,这些都是状态,状态变化时需要 setState() 来重建 Widget
所以Image需要是StatefulWidget

然后解释为什么不是RenderObjectWidget?
和Text一样Image只是一个容器用来管理配置项数据,其内部包含一个RawImage,RawImage是继承RenderObjectWidget的。
源码 _ImageState 核心逻辑(精简):

class _ImageState extends State<Image> {
  ImageStream? _imageStream;
  ImageInfo? _imageInfo;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _resolveImage();
  }

  void _resolveImage() {
    final oldImageStream = _imageStream;
    _imageStream = widget.image?.resolve(createLocalImageConfiguration(context));

    if (_imageStream?.key != oldImageStream?.key) {
      oldImageStream?.removeListener(_handleImageFrame);
      _imageStream?.addListener(_handleImageFrame);
    }
  }

  void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo; // 更新状态 → 触发 rebuild
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_imageInfo != null) {
      return RawImage(
        image: _imageInfo!.image,
        width: widget.width,
        height: widget.height,
        fit: widget.fit,
      );
    } else {
      return Container(); // 占位或加载中
    }
  }
}

(Image 包含 RawImage extends RenderObjectWidget) -> RenderObjectElement -> RenderImage
所有的图片最终都是通过RenderImage绘制的

三棵树

在Flutter渲染的流程中,有三棵重要的树,Widget树,Element树,Render树
Render树负责最终的渲染。

一、Widget树(开发者写出来的)

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

CustomWidget (StatefulWidget)
└── Column
    ├── Text("Count: 0")
    └── ElevatedButton
        └── Text("Add")

二、Element树(Flutter 在内存中真正维护的))

1、Element对象

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

element有几个重要的方法:
mount:将Element插入到Element树中。
update:更新Element对应的Widget。
unmount:将Element从Element树中移除。

2、Element树

StatefulElement (CustomWidget)
└── MultiChildRenderObjectElement (Column)
    ├── StatelessElement (Text)
    └── StatelessElement (ElevatedButton)
        └── StatelessElement (Text)

State 挂在 StatefulElement 上
Element 长期存在
Element 才是 Flutter UI 的“骨架”
当Widget树发生变化时,Element树会比较新旧Widget,决定是更新还是重建

三、Render树(真正画出来的)

1、RenderObject对象

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

示例:
RenderBox是常见的RenderObject,用于处理矩形区域的布局和渲染。
RenderFlex用于实现Flex布局(如Row和Column)。 RenderImage绘制图片到屏幕上。
RenderParagraph绘制文本到屏幕上。

RenderObject类内的两个方法

// 位于文件:FlutterSDK/flutter_flutter/packages/flutter/lib/src/rendering/object.dart内    
void markNeedsLayout();

void markNeedsPaint();

⚠️ 注意:
不是每个 Widget 都有 RenderObject

2、Render树

RenderFlex (Column)
├── RenderParagraph (Text: Count)
└── RenderButton
    └── RenderParagraph (Text: Add)

四、三棵树之间的“一对一关系”

Widget            Element                    RenderObject
----------------------------------------------------------------
CustomWidget →    StatefulElement      →     (无)
Column       →    ColumnElement        →     RenderFlex
Text         →    TextElement          →     RenderParagraph
Button       →    ButtonElement        →     RenderButton

是如何更新的

一、setState发生了什么

当调用

setState(() {
  
});

Flutter 内部真实流程(非常重要)

1️⃣ setState()
   ↓
2️⃣ 标记 CustomWidget 对应的 StatefulElement 为 dirty
   ↓
3️⃣ 下一帧,该 Element 重新执行 build(),内部调用element.state.build
   ↓
4️⃣ 生成新的 Widget Tree
   ↓
5️⃣ 新旧 Widget 对比(diff)[canUpdate方法]
   ↓
6️⃣ true 复用 Element / false 新建 Element
   ↓
7️⃣ 只更新必要的 RenderObject / 新建 RenderObject

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

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

二、canUpdate

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

(1)Widget树
Widget树已经更新为新Widget树。
Widget树上既有fullWidget也有lessWidget.
例如,旧Widget是Text(‘Hello’),新Widget是Text(‘World’)。

(2)Element树
Flutter会复用现有的Element,并调用Element.update方法,更新其对应的Widget引用。
如果是StateFullElement会保留 State 对象。
1、StateFullElement对象持有state,state持有widget
2、旧的state被复用,没有生成新的state,所以更新时新widget的createState方法并没有被调用

(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()创建。
1、如果是StateFullElement对象,它会持有一个state。
2、所以widget的createState方法也会被调用。
3、所以initState方法也会被调用。
4、如果是自定义组件,我们可以通过传入不同的key,让canUpdate返回false,来观察。

例如:
旧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();

一个典型示例

图中实例对象 widgeta 和 widgetb 的组件代码如下:

class StfulItem extends StatefulWidget {
  // ignore: prefer_const_constructors_in_immutables
  StfulItem(this.title, {Key? key}) : super(key: key);

  // 注意 title 是widget的属性
  final String title;

  Color randomColor = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  final _colorInWidget = randomColor;

  @override
  _StfulItemState createState() => _StfulItemState();
}

class _StfulItemState extends State<StfulItem> {

  // 注意:color是state的属性
  Color randomColor = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
  final _colorInSta = randomColor;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100, height: 100,
      // color: widget._colorInWidget,
      color: _colorInState,
      child: Text(widget.title),
    );
  }
}

注意:title是widget的属性。color是state的属性

rebuild的时候会调用canUpdate方法

// 这个是widget的静态方法
// 当widget创建没有指定key时,oldWidget.key == newWidget.key 会被认为是true.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}

根据canUpdate返回值
返回true更新element持有的widget
返回false销毁当前element,创建新的element

如图中:
当我们删除掉widgeta,进行rebuild时
此时的widget树的第一个节点(widgetb)和element树的第一个节点(elementa.statea.widgeta)比较,canUpdate返回true,
这时elementa会更新持有的widget为elementa.statea.widgetb。
所以title发生了变换变为了widgetb,而颜色没有发生变化还是statea持有的颜色。

结合三棵树来说说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.