目录
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。
行者常至,为者常成!