目录
1. Flutter启动流程 和 渲染流程
2. Element、BuildContext、RenderObject、RenderBox
4. 图片加载原理与缓存
5. 布局过程
6. 绘制
1. Flutter启动流程 和 渲染流程
- 启动流程
Flutter程序的入口为lib目录中main.dart文件的main函数(程序的起点)。
main函数最简单的实现如下:
void main() {
runApp(MyApp()); // 只调用了一个runApp()方法
}
=============================
看一下runApp方法的实现:
// 参数app为根widget,是Flutter应用启动后要展示的第一个Widget。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
=============================
看一下WidgetsFlutterBinding类的定义:
//
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
// 负责初始化一个WidgetsBinding的全局单例
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
可以看到WidgetsFlutterBinding继承自BindingBase并混入了很多Binding。
查看这些Binding的源码可以发现这些Binding中基本都是监听并处理Window对象(包含了当前设备和系统的一些信息以及Flutter引擎的一些回调)的一些事件,然后将这些事件按照Framework的模型包装抽象然后分发。
WidgetsFlutterBinding正是粘连Engine引擎层与Framework框架层的“胶水”(绑定Engine引擎层和Framework框架层的桥梁)。
1. GestureBinding:
提供了window.onPointerDataPacket回调,
绑定Framework框架层的手势系统Gestures。是Framework事件模型与底层事件的绑定入口。
2. ServicesBinding:
提供了window.onPlatformMessage回调,
用于绑定平台消息通道,主要处理原生和Flutter通信。
3. SchedulerBinding:
提供了window.onBeginFrame和window.onDrawFrame回调监听刷新事件,
绑定Framework框架层的绘制调度系统。
4. PaintingBinding:
绑定Framework框架层的绘制系统Painting。主要用于处理图片缓存。
5. SemanticsBinding:
绑定Framework框架层的语义化系统。主要是辅助功能的底层支持。
6. RendererBinding:
提供了window.onMetricsChanged、window.onTextScaleFactorChanged等回调。
绑定Framework框架层的渲染系统Rendering。
7. WidgetsBinding:
提供了window.onLocaleChanged、onBuildScheduled等回调。
绑定Framework框架层的组件库Widgets。
=============================
runApp方法在获取到WidgetsBinding单例后,先调用了该单例的attachRootWidget方法:
// 该方法主要完成了根widget到根RenderObject再到根Element的整个关联过程
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView, // 渲染树的根(一个RenderObject)
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement); // renderViewElement是renderView对应的Element对象
}
=============================
attachToRenderTree方法的实现:
//
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
该方法负责创建根element(RenderObjectToWidgetElement),并且将根element与根widget进行关联(即创建出根widget树对应的根element树)。如果根element已经创建过了,则将根element中关联的根widget设为新的并调用markNeedsBuild更新UI。BuildOwner是widget framework的管理类,它跟踪哪些widget需要重新构建。
=============================
runApp方法中接着调用WidgetsBinding单例的scheduleWarmUpFrame方法:
// 该方法的实现在SchedulerBinding中,它被调用后会立即进行一次绘制(而不是等待"vsync" 信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前 Flutter 将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。
void scheduleWarmUpFrame() {
...//
Timer.run(() {
handleBeginFrame(null);
});
Timer.run(() {
handleDrawFrame();
resetEpoch();
});
// 锁定事件
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
...
}
该方法中主要调用了handleBeginFrame() 和 handleDrawFrame() 两个方法,前者主要是执行了transientCallbacks队列,而后者执行了 persistentCallbacks 和 postFrameCallbacks 队列。
=============================
最后看一下上面说到的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;
// 绘制回调
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的一些回调。
- 渲染流程
- Frame 一次绘制(一帧)
Flutter引擎受显示器垂直同步信号"VSync"的驱使不断触发绘制,可以实现60fps(Frame Per-Second)即一秒重绘60次,FPS值越大越流畅。
Flutter中的frame概念并不等同于屏幕的刷新帧Frame,因为Flutter UI框架的frame并不是每次屏幕刷新都会触发。如果UI在一段时间不变,那么每次屏幕刷新都重新走一遍渲染流程是不必要的。因此,Flutter在第一帧渲染结束后会采取一种主动请求frame的方式来实现只有当UI可能会改变时才会重新走渲染流程。
1. Flutter在window上注册了一个onBeginFrame和一个onDrawFrame回调,在onDrawFrame回调中最终会调用drawFrame。
2. 主动调用window.scheduleFrame方法之后,Flutter引擎会在合适的时机(可以认为是在屏幕下一次刷新之前,具体取决于Flutter引擎的实现)来调用onBeginFrame和onDrawFrame。
- SchedulerPhase(Flutter应用执行过程的5种状态)
// Flutter将整个生命周期分为五种状态
enum SchedulerPhase {
idle,
transientCallbacks,
midFrameMicrotasks,
persistentCallbacks,
postFrameCallbacks,
}
Flutter应用执行过程可简单分为2种状态:
整个Flutter应用生命周期就是不断在idle和frame两种状态间切换。
1. idle状态
空闲状态。
表示没有frame在处理(即页面未发生变化,并不需要重新渲染)。
如果应用状态改变需要刷新UI,则需要通过调用scheduleFrame()去请求新的 frame,当frame到来时就进入了frame状态。
注意:空闲状态只是指没有frame在处理,通常微任务、定时器回调或者用户事件回调都可能被执行。比如监听了tap事件,用户点击后onTap回调就是在idle阶段被执行的。
2. frame状态(又分为4种)
当有新的frame到来时,具体处理过程是依次执行四个任务队列:
1. transientCallbacks:
执行临时回调任务(只能被执行一次,执行后会被移出临时任务队列)。
可存放动画回调。
可以通过SchedulerBinding.instance.scheduleFrameCallback添加回调。
2. midFrameMicrotasks:
在执行临时任务时可能会产生一些新的微任务,比如在执行第一个临时任务时创建了一个Future,且这个Future在所有临时任务执行完毕前就已经resolve了,此时Future的回调将在本阶段执行。
3. persistentCallbacks:
执行一些持久任务(每一个frame都要执行的任务)
处理渲染管线(构建、布局、绘制)。不能在这里触发新的Frame。
可以通过SchedulerBinding.instance.addPersitentFrameCallback添加(不能移除)。
4. postFrameCallbacks:
在当前Frame结束之前会被调用一次。
负责清理工作。不能在这里触发新的Frame(会导致无限循环刷新)。
可以通过SchedulerBinding.instance.addPostFrameCallback添加。
当四个任务队列执行完毕后当前frame结束。
- setState执行流程、执行时机问题
执行流程
1. 调用当前element的markNeedsBuild方法,将当前element标记为dirty。
2. 调用scheduleBuildFor,将标记为dirty的当前element添加到pipelineOwner的dirtyElements列表。
3. 请求一个新的frame,然后绘制新frame:
onBuildScheduled--(会调用)->ensureVisualUpdate--(会调用)->scheduleFrame() 。
当新的frame到来时执行渲染管线。
=========================================
执行时机问题:
在transientCallbacks和midFrameMicrotasks阶段,如果应用状态发生变化,最好的方式是只将组件标记为dirty,而不用再去请求新的frame ,因为当前frame还没有执行到persistentCallbacks,因此后面执行到后就会在当前帧渲染管线中刷新UI。因此,setState在标记完dirty后会先判断一下调度状态,如果是idle或postFrameCallbacks阶段才会去请求新的frame :
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); // 请求新的frame
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks: // 注意这一行
return;
}
}
如果在build阶段调用setState的话就又会导致调用build(无限循环调用),因此flutter框架发现在build阶段调用setState的话就会报错,如:
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, c) {
// build阶段不能调用setState, 会报错
setState(() {
++index;
});
return Text('xx');
},
);
}
/*
注意:如果直接在build中调用setState(和上面的上下文不一样):
@override
Widget build(BuildContext context) {
setState(() {
++index;
});
return Text('$index');
}
运行后是不会报错的,原因是在执行build时当前组件对应的element的dirty状态为true,只有build执行完后才会被置为false。而setState执行时会先判断当前dirty值,如果为true则会直接返回,因此就不会报错。
【存疑】
*/
不止build阶段,在persistentCallbacks(build构建、布局、绘制)阶段都不能调用setState,会导致循环调用。因此如果要在这些阶段更新应用状态时,不能直接调用setState,可使用如下方法来安全更新:
// 自定义一个update方法(可以安全更新状态)
// 如果是persistentCallbacks阶段,则调用了SchedulerBinding.instance的addPostFrameCallback。
// 如果是其他阶段,则直接调用setState。
void update(VoidCallback fn) {
final schedulerPhase = SchedulerBinding.instance.schedulerPhase;
if (schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
} else {
setState(fn);
}
}
-
渲染管线(渲染流程)
setState方法内部会调用scheduleFrame方法去请求新的frame,当新frame到来时 会调用WidgetsBinding的drawFrame() 方法:
//
@override
void drawFrame() {
...// 省略无关代码
try {
buildOwner.buildScope(renderViewElement); // 1. 重新构建widget树
super.drawFrame(); // 调用父类的drawFrame()方法,具体实现见下方注释
/*
super.drawFrame()的方法实现如下:
pipelineOwner.flushLayout(); // 2. 更新布局
pipelineOwner.flushCompositingBits(); // 3. 更新“层合成”信息
pipelineOwner.flushPaint(); // 4. 重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
pipelineOwner.flushSemantics();
_firstFrameSent = true;
}
*/
}
}
主要作了5件事:
1. 重新构建widget树。
如果 dirtyElements 列表不为空,则遍历该列表,调用每一个element的rebuild方法重新构建新的widget树。
由于新的widget树使用新的状态构建,所以可能导致widget布局信息(占用的空间和位置)发生变化,如果发生变化,则会调用其renderObject的markNeedsLayout方法,该方法会从当前节点向父级查找,直到找到一个relayoutBoundary的节点,然后会将它添加到一个全局的nodesNeedingLayout列表中;如果直到根节点也没有找到relayoutBoundary,则将根节点添加到nodesNeedingLayout列表中。
2. 更新布局。
遍历nodesNeedingLayout数组,对每一个renderObject重新布局(调用其layout方法),确定新的大小和偏移。layout方法中会调用markNeedsPaint方法(和markNeedsLayout方法功能类似),也会从当前节点向父级查找,直到找到一个isRepaintBoundary属性为true的父节点,然后将它添加到一个全局的nodesNeedingPaint列表中;由于根节点(RenderView)的 isRepaintBoundary 为 true,所以必会找到一个。查找过程结束后会调用 buildOwner.requestVisualUpdate 方法,该方法最终会调用scheduleFrame(),该方法中会先判断是否已经请求过新的frame,如果没有则请求一个新的frame。
3. 更新“层合成”信息。
4. 重绘。
遍历nodesNeedingPaint列表,调用每一个节点的paint方法进行重绘,绘制过程会生成Layer。flutter中绘制结果是保存在Layer中的,只要Layer不释放,绘制的结果就会被缓存。因此,Layer可以跨frame来缓存绘制结果,避免不必要的重绘开销。
Flutter框架绘制过程中,遇到isRepaintBoundary 为 true 的节点时,才会生成一个新的Layer。可见Layer和 renderObject 不是一一对应关系,父子节点可以共享layer。如果是自定义组件,我们可以在renderObject中手动添加任意多个 Layer,这通常用于只需一次绘制而随后不会发生变化的绘制元素的缓存场景。
5. 上屏:将绘制的产物显示在屏幕上
绘制完成后得到的是一棵Layer树,最后需要将Layer树中的绘制信息在屏幕上显示。Flutter是自实现的渲染引擎,因此需要将绘制信息提交给Flutter engine,而renderView.compositeFrame正是完成了这个使命。
// 在上屏时会将所有的Layer添加到Scene场景对象后,再渲染Scene。
上面5步被称为渲染管线(rendering pipeline)或渲染流水线。
渲染绘制的具体逻辑在RendererBinding中实现。查看其源码,发现在其initInstances()方法中有如下代码:
void initInstances() {
... // 省略无关代码
// 监听Window对象的事件
ui.window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
// 通过addPersistentFrameCallback 向persistentCallbacks队列添加了一个回调 _handlePersistentFrameCallback
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 布局
pipelineOwner.flushCompositingBits(); // 重绘之前的预处理操作,检查RenderObject是否需要重绘
pipelineOwner.flushPaint(); // 重绘
renderView.compositeFrame(); // 上屏(将需要绘制的比特数据发给GPU)
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
===================================
flushLayout() 方法主要任务是更新了所有被标记为“dirty”的RenderObject的布局信息。主要的动作发生在node._layoutWithoutResize()方法中,该方法中会调用performLayout()进行重新布局。
void flushLayout() {
...
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (RenderObject node in
dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
}
}
===================================
flushCompositingBits()方法 检查RenderObject是否需要重绘,然后更新RenderObject.needsCompositing属性(如果该属性值被标记为true则需要重绘)。
void flushCompositingBits() {
_nodesNeedingCompositingBitsUpdate.sort(
(RenderObject a, RenderObject b) => a.depth - b.depth
);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits(); // 更新RenderObject.needsCompositing属性值
}
_nodesNeedingCompositingBitsUpdate.clear();
}
===================================
flushPaint() 方法进行了最终的绘制,可以看出它不是重绘了所有 RenderObject,而是只重绘了需要重绘的 RenderObject。真正的绘制是通过PaintingContext.repaintCompositedChild()来绘制的,该方法最终会调用Flutter engine提供的Canvas API来完成绘制。
void flushPaint() {
...
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// 反向遍历需要重绘的RenderObject
for (RenderObject node in
dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
// 真正的绘制逻辑
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
}
}
===================================
compositeFrame() 方法中有一个Scene对象,Scene对象是一个数据结构,保存最终渲染后的像素信息。这个方法将Canvas画好的Scene传给window.render()方法,该方法会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上。
void compositeFrame() {
...
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
ui.window.render(scene); //调用Flutter engine的渲染API
scene.dispose();
} finally {
Timeline.finishSync();
}
}
2. Element、BuildContext、RenderObject、RenderBox
- Element
Flutter的UI框架包含三棵树:Widget树、Element树、渲染树。
最终的UI树由一个个独立的Element节点构成。Widget组件的Layout、渲染最终都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。Flutter正是通过Element这个纽带将Widget和RenderObject关联起来。
Element是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类(如:MultiChildRenderObjectElement等)。最终所有Element的RenderObject构成一棵渲染树(Render Tree)。
对于开发者来说,大多数情况下只需要关注Widget树,Flutter框架已经将对Widget树的操作映射到了Element树上,极大的降低了复杂度,提高了开发效率。
但有时候必须得直接使用Element对象来完成一些操作,比如获取主题Theme数据。
Element的生命周期如下:
1. Framework 调用Widget.createElement 创建一个Element实例element。
2. Framework 调用element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
3. 当有父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来避免复用。
4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。
5. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。
6. 如果element要重新插入到Element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。
- BuildContext
abstract class BuildContext {
...
}
context可用于:
1. Theme.of(context) // 获取主题
2. Navigator.push(context, route) // 入栈新路由
3. Localizations.of(context, type) // 获取Local
4. context.size // 获取大小
5. context.findRenderObject() // 查找当前或最近的一个祖先RenderObject
StatelessWidget和StatefulWidget的build方法都会传一个BuildContext对象(即Element对象)
Widget build(BuildContext context) {}
该build方法在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build方法中被调用。例(StatelessElement):
class StatelessElement extends ComponentElement {
@override
Widget build() => widget.build(this); // 这里调用了Widget的build方法
}
build传递的参数是this,所有这个BuildContext就是StatelessElement。但StatelessElement和StatefulElement本身并没有实现BuildContext接口。继续跟踪代码,发现它们间接继承自Element类,然后查看Element类定义,发现Element类果然实现了BuildContext接口:
class Element extends DiagnosticableTree implements BuildContext {
}
至此真相大白,BuildContext就是widget对应的Element,所以可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。
可以看到Element是Flutter UI框架内部连接widget和RenderObject的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数又将Element对象也传递给了开发者。这样一来,开发者便可以在需要时直接操作Element对象。
完全可以直接通过Element来搭建一个UI框架,但使用Widget更方便。
例(通过纯粹的Element来模拟一个StatefulWidget的功能)
假设有一个页面,该页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排一次序
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
@override
Widget build() {
Color primary=Theme.of(this).primaryColor; // 1
return GestureDetector(
child: Center(
child: FlatButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); // 点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
说明:
1. 上面build方法不接收参数,这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用this代替即可,因为当前对象本身就是Element实例。
2. 当text发生改变时,调用markNeedsBuild()方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法。
3. 上面代码中build方法返回的仍然是一个widget,再加上适配器就可以和其他组件组合使用。如果UI全是由Element组成,则这里返回类型应为Element。
如果需要将上面代码在现有Flutter框架中跑起来,那么还得提供一个“适配器”widget将HomeView结合到现有框架中,下面CustomHome就相当于“适配器”:
class CustomHome extends Widget {
@override
Element createElement() {
return HomeView(this);
}
}
点击按钮则按钮文本会随机排序。
- RenderObject和RenderBox
每个Element都对应一个RenderObject,可以通过Element.renderObject来获取。
RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树。
RenderObject就是渲染树中的一个对象,它主要的作用是实现事件响应以及渲染管线中除过 build 的部分(build 部分由 element 实现),即包括:布局、绘制、层合成以及上屏。
RenderObject拥有一个parent(渲染树中自己的父节点)和一个parentData(slot插槽:一个预留变量)。
在父组件的布局过程,会确定其所有子组件布局信息(如位置信息,即相对于父组件的偏移),而这些布局信息需要在布局阶段保存起来,因为布局信息在后续的绘制阶段还需要被使用(用于确定组件的绘制位置),而parentData属性的主要作用就是保存布局信息,比如在Stack 布局中,RenderStack就会将子元素的偏移数据存储在子元素的parentData中。
RenderObject类本身实现了一套基础的布局和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。
为此,Flutter提供了一个RenderBox类和一个 RenderSliver类,都继承自RenderObject,布局坐标系统采用笛卡尔坐标系。Flutter 基于这两个类分别实现了基于 RenderBox 的盒模型布局和基于 Sliver 的按需加载模型。
如果要从头到尾实现一个RenderObject是比较麻烦的(必须去实现layout、绘制和命中测试逻辑),大多数时候可以直接在Widget层通过组合或者CustomPaint完成自定义UI。如果遇到只能定义一个新RenderObject的场景时(如要实现一个新的layout算法的布局容器),可以直接继承自RenderBox去实现。
4. 图片加载原理与缓存
Flutter框架对加载过的图片是有缓存的(内存),默认最大缓存数量是1000,最大缓存空间为100M。
ImageProvider主要负责图片数据的加载和缓存,而绘制部分逻辑主要是由RawImage来完成。
Image是连接起ImageProvider和RawImage的桥梁。
ImageProvider
Image组件的image参数是一个ImageProvider类型的必选参数。
ImageProvider是一个抽象类,定义了图片数据获取、加载、缓存的相关接口。
abstract class ImageProvider<T> {
ImageStream resolve(ImageConfiguration configuration) {
...
}
Future<bool> evict({ ImageCache cache,
ImageConfiguration configuration = ImageConfiguration.empty }) async {
...
}1
Future<T> obtainKey(ImageConfiguration configuration);
@protected
ImageStreamCompleter load(T key); // 需子类实现
}
- load(T key)方法
加载图片数据源的接口,不同的数据源的加载方法不同,每个ImageProvider的子类必须实现它。
比如NetworkImage类和AssetImage类,它们都是ImageProvider的子类,但它们需要从不同的数据源来加载图片数据:NetworkImage是从网络来加载图片数据,而AssetImage则是从应用安装包来加载图片数据。
以NetworkImage为例,看看其load方法的实现:
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), // 调用
chunkEvents: chunkEvents.stream,
scale: key.scale,
... // 省略无关代码
);
}
load方法的返回值类型是ImageStreamCompleter ,它是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中正是通过它来监听图片加载状态的。MultiFrameImageStreamCompleter 是 ImageStreamCompleter的一个子类,是flutter sdk预置的类,通过该类可以方便、轻松地创建出一个ImageStreamCompleter实例来做为load方法的返回值。
MultiFrameImageStreamCompleter 需要一个codec参数,该参数类型为Future<ui.Codec>。Codec 是处理图片编解码的类的一个handler,实际上它只是一个flutter engine API 的包装类,也就是说图片的编解码逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的。
==========================================
==========================================
Codec类部分定义如下:
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
@pragma('vm:entry-point')
Codec._();
/// 图片中的帧数(动态图会有多帧)
int get frameCount native 'Codec_frameCount';
/// 动画重复的次数
/// * 0 表示只执行一次
/// * -1 表示循环执行
int get repetitionCount native 'Codec_repetitionCount';
/// 获取下一个动画帧
Future<FrameInfo> getNextFrame() {
return _futurize(_getNextFrame);
}
// Codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。
String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
==========================================
==========================================
MultiFrameImageStreamCompleter 的 codec参数值为_loadAsync方法的返回值,_loadAsync方法的实现:
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
// 下载图片
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw Exception(...);
// 接收图片数据
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
// 对图片数据进行解码
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
}
}
可以看到_loadAsync方法主要做了两件事:
1. 下载图片。通过HttpClient从网上下载图片,下载请求会设置一些自定义的header,开发者可以通过NetworkImage的headers命名参数来传递。
2. 对下载的图片数据进行解码。在图片下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码,值得注意的是instantiateImageCodec(...)也是一个Native API的包装,实际上会调用Flutter engine的instantiateImageCodec方法,源码如下:
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight) native 'instantiateImageCodec';
- obtainKey(ImageConfiguration)方法
配合实现图片缓存,ImageProvider从数据源加载完数据后,会在全局的ImageCache中缓存图片数据,而图片数据缓存是一个Map,而Map的key便是调用此方法的返回值,不同的key代表不同的图片数据缓存。
以NetworkImage为例,看一下它的obtainKey()实现:
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
该方法创建了一个同步的future,然后直接将自身做为key返回。因为Map中在判断key(此时是NetworkImage对象)是否相等时会使用“==”运算符,那么定义key的逻辑就是NetworkImage的“==”运算符:
@override
bool operator ==(dynamic other) {
... // 省略无关代码
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
对于网络图片来说,会将其“url地址+scale缩放比例”作为缓存的key。也就是说如果两张图片的url或scale只要有一个不同,便会重新下载并分别缓存。
需要注意的是,图片缓存是在内存中,并没有进行本地文件持久化存储,这也是为什么网络图片在应用重启后需要重新联网下载的原因。同时也意味着在应用生命周期内,如果缓存没有超过上限,相同的图片只会被下载一次。
- resolve(ImageConfiguration) 方法
resolve方法是ImageProvider暴露给Image的主入口方法,接受一个ImageConfiguration参数,返回ImageStream图片数据流。
ImageStream resolve(ImageConfiguration configuration) {
... // 省略无关代码
final ImageStream stream = ImageStream();
T obtainedKey; //
// 定义错误处理函数
Future<void> handleError(dynamic exception, StackTrace stack) async {
... // 省略无关代码
stream.setCompleter(imageCompleter);
imageCompleter.setError(...);
}
// 创建一个新Zone,主要是为了当发生错误时不会干扰MainZone
final Zone dangerZone = Zone.current.fork(...);
dangerZone.runGuarded(() {
Future<T> key;
// 先验证是否已经有缓存
try {
// 生成缓存key,后面会根据此key来检测是否有缓存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
// 处理缓存,这里的PaintingBinding.instance.imageCache 是 ImageCache的一个实例,它是PaintingBinding的一个属性,而Flutter框架中的PaintingBinding.instance是一个单例,imageCache事实上也是一个单例,也就是说图片缓存是全局的,统一由PaintingBinding.instance.imageCache 来管理。
final ImageStreamCompleter completer = PaintingBinding.instance
.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
});
return stream;
}
==========================================
==========================================
ImageConfiguration包含了图片和设备的相关信息,如图片的大小、所在的AssetBundle(只有打到安装包的图片存在)以及当前的设备平台、devicePixelRatio(设备像素比)。
Flutter SDK提供了一个便捷函数createLocalImageConfiguration来创建ImageConfiguration 对象:
ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {
return ImageConfiguration(
bundle: DefaultAssetBundle.of(context),
devicePixelRatio: MediaQuery.of(context, nullOk: true)?.devicePixelRatio ?? 1.0,
locale: Localizations.localeOf(context, nullOk: true),
textDirection: Directionality.of(context),
size: size,
platform: defaultTargetPlatform,
);
}
这些信息基本都是通过Context来获取。
==========================================
==========================================
ImageCache类定义:
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
// 正在加载中的图片队列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 缓存队列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 缓存数量上限(1000)
int _maximumSize = _kDefaultSize;
// 缓存容量上限 (100 MB)
int _maximumSizeBytes = _kDefaultSizeBytes;
// 缓存上限设置的setter
set maximumSize(int value) {...}
set maximumSizeBytes(int value) {...}
... // 省略部分定义
// 清除所有缓存
void clear() {
// ...省略具体实现代码
}
// 清除指定key对应的图片缓存
bool evict(Object key) {
// ...省略具体实现代码
}
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
assert(key != null);
assert(loader != null);
ImageStreamCompleter result = _pendingImages[key]?.completer;
// 图片还未加载成功,直接返回
if (result != null)
return result;
// 1. 先判断图片数据有没有缓存,如果有则先移除缓存后再添加(可以让最新使用过的缓存在_map中的位置更近一些),并返回ImageStream。
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
// 2. 如果没有缓存,则调用load(T key)方法从数据源加载图片数据,加载成功后先缓存,然后返回ImageStream。
try {
result = loader();
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
void listener(ImageInfo info, bool syncCall) {
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
// 下面是缓存处理的逻辑
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
_cache[key] = image;
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
}
return result;
}
// 当缓存数量超过最大值或缓存的大小超过最大缓存容量,会调用此方法清理到缓存上限以内
void _checkCacheSize() {
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key];
_currentSizeBytes -= image.sizeBytes;
_cache.remove(key);
}
... //省略无关代码
}
}
可以自定义缓存上限:
PaintingBinding.instance.imageCache.maximumSize=2000; // 最多2000张
PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; // 最大200M
因为Map中相同key的值会被覆盖,也就是说key是图片缓存的一个唯一标识,只要是不同key,那么图片数据就会分别缓存(即使事实上是同一张图片)。key是ImageProvider.obtainKey()方法的返回值,此方法需要ImageProvider子类去重写,这也就意味着不同的ImageProvider对key的定义逻辑会不同。比如对于NetworkImage,将图片的url和scale作为key会很合适,而对于AssetImage则应该将“包名+路径”作为唯一的key。
Image组件原理
通过实现一个“简版的Image组件”,来大致了解Image组件原理。
代码流程如下:
1. 通过imageProvider.resolve方法可以得到一个ImageStream(图片数据流),然后监听ImageStream的变化。当图片数据源发生变化时,ImageStream会触发相应的事件,而本例中只设置了图片成功的监听器_updateImage,而_updateImage中只更新了_imageInfo。值得注意的是,如果是静态图,ImageStream只会触发一次时间,如果是动态图,则会触发多次事件,每一次都会有一个解码后的图片帧。
2. _imageInfo 更新后会rebuild,此时会创建一个RawImage Widget。RawImage最终会通过RenderImage来将图片绘制在屏幕上。如果继续跟进RenderImage类,会发现RenderImage的paint 方法中调用了paintImage方法,而paintImage方法中通过Canvas的drawImageRect(…)、drawImageNine(...)等方法来完成最终的绘制。
class MyImage extends StatefulWidget {
final ImageProvider imageProvider;
const MyImage({
Key key,
@required this.imageProvider,
})
: assert(imageProvider != null),
super(key: key);
@override
_MyImageState createState() => _MyImageState();
}
class _MyImageState extends State<MyImage> {
ImageStream _imageStream;
ImageInfo _imageInfo;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 依赖改变时,图片的配置信息可能会发生改变
_getImage();
}
@override
void didUpdateWidget(MyImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageProvider != oldWidget.imageProvider)
_getImage();
}
void _getImage() {
final ImageStream oldImageStream = _imageStream;
// 调用imageProvider.resolve方法,获得ImageStream。
_imageStream =
widget.imageProvider.resolve(createLocalImageConfiguration(context));
// 判断新旧ImageStream是否相同,如果不同,则需要调整流的监听器
if (_imageStream.key != oldImageStream?.key) {
final ImageStreamListener listener = ImageStreamListener(_updateImage);
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}
@override
void dispose() {
_imageStream.removeListener(ImageStreamListener(_updateImage));
super.dispose();
}
@override
Widget build(BuildContext context) {
return RawImage( // dart:ui库
image: _imageInfo?.image,
scale: _imageInfo?.scale ?? 1.0,
);
}
}
测试一下MyImage组件
class ImageInternalTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
MyImage(
imageProvider: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
),
)
],
);
}
}
5. 布局过程
布局过程主要是确定每一个组件的布局信息(大小和位置)
Flutter 的布局过程如下:
1. 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
2. 子节点根据约束信息确定自己的大小(size)。
3. 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
4. 递归整个过程,确定出每一个节点的大小和位置。
可以看到,组件的大小是由自身决定的,而组件的位置是由父组件决定的。
示例(SingleChildRenderObjectWidget)
Constraints
在RenderBox 中,有个size属性用来保存控件的宽和高。RenderBox的layout是通过在组件树中从上往下传递BoxConstraints对象的实现的。BoxConstraints对象可以限制子节点的最大和最小宽高,子节点必须遵守父节点给定的限制条件。
在布局阶段,父节点会调用子节点的layout()方法。
RenderObject中layout()方法的大致实现:
void layout(Constraints constraints, { bool parentUsesSize = false }) {
...
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight
|| parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
...
if (sizedByParent) {
performResize();
}
performLayout();
...
}
layout方法需要传入两个参数,第一个为constraints,即 父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。另外一个参数是 parentUsesSize,该值用于确定 relayoutBoundary,该参数表示子节点布局变化是否影响父节点,如果为true,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为false,则子节点布局发生变化后不会影响父节点。
relayoutBoundary
上面layout()源码中定义了一个relayoutBoundary变量,
当一个Element标记为 dirty(通过调用 markNeedsBuild() 方法)时便会重新build,这时RenderObject便会重新布局。在RenderObject中有一个类似的markNeedsLayout()方法,它会将RenderObject的布局状态标记为 dirty,这样在下一个frame中便会重新layout,
RenderObject的markNeedsLayout()的部分源码:
void markNeedsLayout() {
...
assert(_relayoutBoundary != null);
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
...
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
代码大致逻辑是先判断自身是不是relayoutBoundary,如果不是就继续向parent 查找,一直向上查找到是 relayoutBoundary 的 RenderObject为止,然后再将其标记为 dirty 的。这样来看它的作用就比较明显了,意思就是当一个控件的大小被改变时可能会影响到它的 parent,因此 parent 也需要被重新布局,那么到什么时候是个头呢?答案就是 relayoutBoundary,如果一个 RenderObject 是 relayoutBoundary,就表示它的大小变化不会再影响到 parent 的大小了,于是 parent 也就不用重新布局了。
performResize 和 performLayout
RenderBox实际的测量和布局逻辑是在performResize() 和 performLayout()两个方法中,RenderBox子类需要实现这两个方法来定制自身的布局逻辑。
根据layout() 源码可以看出只有 sizedByParent 为 true 时,performResize() 才会被调用,而 performLayout() 是每次布局都会被调用的。sizedByParent 意为该节点的大小是否仅通过 parent 传给它的 constraints 就可以确定了,即该节点的大小与它自身的属性和其子节点无关,比如如果一个控件永远充满 parent 的大小,那么 sizedByParent就应该返回true,此时其大小在 performResize() 中就确定了,在后面的 performLayout() 方法中将不会再被修改了,这种情况下 performLayout() 只负责布局子节点。
在 performLayout() 方法中除了完成自身布局,也必须完成子节点的布局,这是因为只有父子节点全部完成后布局流程才算真正完成。所以最终的调用栈将会变成:layout() > performResize()/performLayout() > child.layout() > ... ,如此递归完成整个UI的布局。
RenderBox子类要定制布局算法不应该重写layout()方法,因为对于任何RenderBox的子类来说,它的layout流程基本是相同的,不同之处只在具体的布局算法,而具体的布局算法子类应该通过重写performResize() 和 performLayout()两个方法来实现,他们会在layout()中被调用。
ParentData
RenderObject的parentData 只能通过父元素设置.
当layout结束后,每个节点的位置(相对于父节点的偏移)就已经确定了,RenderObject就可以根据位置信息来进行最终的绘制。但是在layout过程中,节点的位置信息怎么保存?对于大多数RenderBox子类来说如果子类只有一个子节点,那么子节点偏移一般都是Offset.zero ,如果有多个子节点,则每个子节点的偏移就可能不同。而子节点在父节点的偏移数据正是通过RenderObject的parentData属性来保存的。在RenderBox中,其parentData属性默认是一个BoxParentData对象,该属性只能通过父节点的setupParentData()方法来设置:
abstract class RenderBox extends RenderObject {
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! BoxParentData)
child.parentData = BoxParentData();
}
...
}
BoxParentData定义如下:
/// Parentdata 会被RenderBox和它的子类使用.
class BoxParentData extends ParentData {
/// offset表示在子节点在父节点坐标系中的绘制偏移
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
ParentData并不仅仅可以用来存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的ParentData中,如ContainerBox的ParentData就保存了指向兄弟节点的previousSibling和nextSibling,Element.visitChildren()方法也正是通过它们来实现对子节点的遍历。再比如KeepAlive 组件,它使用KeepAliveParentDataMixin(继承自ParentData) 来保存子节的keepAlive状态。
- 绘制过程
RenderObject可以通过paint()方法来完成具体绘制逻辑,流程和布局流程相似,子类可以实现paint()方法来完成自身的绘制逻辑,paint()签名如下:
void paint(PaintingContext context, Offset offset) { }
通过context.canvas可以取到Canvas对象,就可以调用Canvas API来实现具体的绘制逻辑
如果节点有子节点,它除了完成自身绘制逻辑之外,还要调用子节点的绘制方法。例:
@override
void paint(PaintingContext context, Offset offset) {
// 如果子元素未超出当前边界,则绘制子元素
if (_overflow <= 0.0) {
defaultPaint(context, offset);
return;
}
// 如果size为空,则无需绘制
if (size.isEmpty)
return;
// 剪裁掉溢出边界的部分
context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint);
assert(() {
final String debugOverflowHints = '...'; //溢出提示内容,省略
// 绘制溢出部分的错误提示样式
Rect overflowChildRect;
switch (_direction) {
case Axis.horizontal:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
break;
case Axis.vertical:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
break;
}
paintOverflowIndicator(context, offset, Offset.zero & size,
overflowChildRect, overflowHints: debugOverflowHints);
return true;
}());
}
代码很简单,首先判断有无溢出,如果没有则调用defaultPaint(context, offset)来完成绘制,该方法源码如下:
void defaultPaint(PaintingContext context, Offset offset) {
ChildType child = firstChild;
while (child != null) {
final ParentDataType childParentData = child.parentData;
//绘制子节点,
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
很明显,由于Flex本身没有需要绘制的东西,所以直接遍历其子节点,然后调用paintChild()来绘制子节点,同时将子节点ParentData中在layout阶段保存的offset加上自身偏移作为第二个参数传递给paintChild()。而如果子节点还有子节点时,paintChild()方法还会调用子节点的paint()方法,如此递归完成整个节点树的绘制,最终调用栈为: paint() > paintChild() > paint() ... 。
当需要绘制的内容大小溢出当前空间时,将会执行paintOverflowIndicator() 来绘制溢出部分提示,这个就是我们经常看到的溢出提示
RepaintBoundary
与 RelayoutBoundary 相似,RepaintBoundary是用于在确定重绘边界的,与RelayoutBoundary不同的是,这个绘制边界需要由开发者通过RepaintBoundary 组件自己指定,如:
CustomPaint(
size: Size(300, 300), //指定画布大小
painter: MyPainter(),
child: RepaintBoundary(
child: Container(...),
),
),
RenderObject有一个isRepaintBoundary属性,该属性决定这个RenderObject重绘时是否独立于其父元素,如果该属性值为true ,则独立绘制,反之则一起绘制。
独立绘制是怎么实现的,答案就在paintChild()源码中:
void paintChild(RenderObject child, Offset offset) {
...
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
...
}
可以看到,在绘制子节点时,如果child.isRepaintBoundary 为 true则会调用_compositeChild()方法,_compositeChild()源码如下:
void _compositeChild(RenderObject child, Offset offset) {
// 给子节点创建一个layer ,然后再上面绘制子节点
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
...
}
assert(child._layer != null);
child._layer.offset = offset;
appendLayer(child._layer);
}
独立绘制是通过在不同的layer(层)上绘制的。所以,很明显,正确使用isRepaintBoundary属性可以提高绘制效率,避免不必要的重绘。具体原理是:和触发重新build和layout类似,RenderObject也提供了一个markNeedsPaint()方法,其源码如下:
void markNeedsPaint() {
...
//如果RenderObject.isRepaintBoundary 为true,则该RenderObject拥有layer,直接绘制
if (isRepaintBoundary) {
...
if (owner != null) {
//找到最近的layer,绘制
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
// 没有自己的layer, 会和一个祖先节点共用一个layer
assert(_layer == null);
final RenderObject parent = this.parent;
// 向父级递归查找
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
// 如果直到根节点也没找到一个Layer,那么便需要绘制自身,因为没有其它节点可以绘制根节点。
if (owner != null)
owner.requestVisualUpdate();
}
}
可以看出,当调用 markNeedsPaint() 方法时,会从当前 RenderObject 开始一直向父节点查找,直到找到 一个isRepaintBoundary 为 true的RenderObject 时,才会触发重绘,这样便可以实现局部重绘。当 有RenderObject 绘制的很频繁或很复杂时,可以通过RepaintBoundary Widget来指定isRepaintBoundary 为 true,这样在绘制时仅会重绘自身而无需重绘它的 parent,如此便可提高性能。
还有一个问题,通过RepaintBoundary 如何设置isRepaintBoundary属性呢?其实,如果使用了RepaintBoundary,其对应的RenderRepaintBoundary会自动将isRepaintBoundary设为true的:
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox child }) : super(child);
@override
bool get isRepaintBoundary => true;
}
- 命中测试
一个对象是否可以响应事件,取决于其对命中测试的返回,当发生用户事件时,会从根节点(RenderView)开始进行命中测试
RenderView的hitTest()源码:
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position); // 递归子RenderBox进行命中测试
result.add(HitTestEntry(this)); // 将测试结果添加到result中
return true;
}
RenderBox默认的hitTest()实现:
bool hitTest(HitTestResult result, { @required Offset position }) {
...
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
默认的实现里调用了hitTestSelf()和hitTestChildren()两个方法,这两个方法默认实现如下:
@protected
bool hitTestSelf(Offset position) => false;
@protected
bool hitTestChildren(HitTestResult result, { Offset position }) => false;
hitTest 方法用来判断该RenderObject 是否在被点击的范围内,同时负责将被点击的 RenderBox 添加到 HitTestResult 列表中,参数 position 为事件触发的坐标(如果有的话),返回 true 则表示有RenderBox 通过了命中测试,需要响应事件,反之则认为当前RenderBox没有命中。在继承RenderBox时,可以直接重写hitTest()方法,也可以重写 hitTestSelf() 或 hitTestChildren(), 唯一不同的是 hitTest()中需要将通过命中测试的节点信息添加到命中测试结果列表中,而 hitTestSelf() 和 hitTestChildren()则只需要简单的返回true或false。
- Semantics语义化
语义化,主要是提供给读屏软件的接口,也是实现辅助功能的基础,通过语义化接口可以让机器理解页面上的内容,对于有视力障碍用户可以使用读屏软件来理解UI内容。
如果一个RenderObject要支持语义化接口,可以实现 describeApproximatePaintClip和 visitChildrenForSemantics方法和semanticsAnnotator getter。
6. 绘制
Flutter 绘制原理
绘制相关的对象:
1. Canvas
封装了Flutter Skia各种绘制指令,比如画线、画圆、画矩形等指令。
2. Layer
分为容器类和绘制类两种;暂时可以理解为是绘制产物的载体,比如调用 Canvas 的绘制 API 后,相应的绘制产物被保存在 PictureLayer.picture 对象中。
3. Scene
屏幕上将要要显示的元素。在上屏前,需要将Layer中保存的绘制产物关联到 Scene 上。
绘制流程:
1. 构建一个 Canvas,用于绘制;同时还需要创建一个绘制指令记录器,因为绘制指令最终是要传递给 Skia 的,而 Canvas 可能会连续发起多条绘制指令,指令记录器用于收集 Canvas 在一段时间内所有的绘制指令,因此Canvas 构造函数第一个参数必须传递一个 PictureRecorder 实例。
2. Canvas 绘制完成后,通过 PictureRecorder 获取绘制产物,然后将其保存在 Layer 中。
3. 构建 Scene 对象,将 layer 的绘制产物和 Scene 关联起来。
4. 上屏;调用window.render API 将Scene上的绘制产物发送给GPU。
示例(五子棋)
void main() {
// 1.创建绘制记录器和Canvas
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
// 2.在指定位置区域绘制。
var rect = Rect.fromLTWH(30, 200, 300,300 );
drawChessboard(canvas,rect); //画棋盘
drawPieces(canvas,rect);//画棋子
// 3.创建layer,将绘制的产物保存在layer中
var pictureLayer = PictureLayer(rect);
//recorder.endRecording()获取绘制产物。
pictureLayer.picture = recorder.endRecording();
var rootLayer = OffsetLayer();
rootLayer.append(pictureLayer);
// 4.上屏,将绘制的内容显示在屏幕上。
final SceneBuilder builder = SceneBuilder();
final Scene scene = rootLayer.buildScene(builder);
window.render(scene);
}
Picture
PictureLayer 的绘制产物是 Picture。
1. Picture 实际上是一系列的图形绘制操作指令。
2. Picture 要显示在屏幕上,必然会经过光栅化,随后Flutter会将光栅化后的位图信息缓存起来,也就是说同一个 Picture 对象,其绘制指令只会执行一次,执行完成后绘制的位图就会被缓存起来。
综合以上两点,可以看到 PictureLayer 的“绘制产物”一开始是一些列“绘图指令”,当第一次绘制完成后,位图信息就会被缓存,绘制指令也就不会再被执行了,所以这时“绘制产物”就是位图了。Picture有一个toImage方法,可以根据指定的大小导出Image。
// 将图片导出为Uint8List
final Image image = await pictureLayer.picture.toImage();
final ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
final Uint8List pngBytes = byteData!.buffer.asUint8List();
print(pngBytes);
Layer
思考一个问题:Layer作为绘制产物的持有者有什么作用? 答案就是:
1. 可以在不同的frame之间复用绘制产物(如果没有发生变化)。
2. 划分绘制边界,缩小重绘范围。
Layer类型
在五子棋示例中定义了两个Layer对象:
1. OffsetLayer
根 Layer,它继承自ContainerLayer,而ContainerLayer继承自 Layer 类,我们将直接继承自ContainerLayer 类的 Layer 称为容器类Layer,容器类 Layer 可以添加任意多个子Layer。
2. PictureLayer
保存绘制产物的 Layer,它直接继承自 Layer 类。我们将可以直接承载(或关联)绘制结果的 Layer 称为绘制类 Layer。
容器类 Layer
作用和具体使用场景:
1. 将组件树的绘制结构组成一棵树。
因为 Flutter 中的 Widget 是树状结构,那么相应的 RenderObject 对应的绘制结构也应该是树状结构,Flutter 会根据一些“特定的规则”为组件树生成一棵 Layer 树,而容器类Layer就可以组成树状结构。
2. 可以对多个 layer 整体应用一些变换效果。
容器类 Layer 可以对其子 Layer 整体做一些变换效果,比如剪裁效果(ClipRectLayer、ClipRRectLayer、ClipPathLayer)、过滤效果(ColorFilterLayer、ImageFilterLayer)、矩阵变换(TransformLayer)、透明变换(OpacityLayer)等。
虽然 ContainerLayer 并非抽象类,开发者可以直接创建 ContainerLayer 类的示例,但实际上很少会这么做,相反,在需要使用使用 ContainerLayer 时直接使用其子类即可。如果我们确实不需要任何变换效果,那么就使用 OffsetLayer,不用担心会有额外性能开销,它的底层(Skia 中)实现是非常高效的。
绘制类 Layer
PictureLayer类是Flutter中最常用的一种绘制类Layer。
最终显示在屏幕上的是位图信息,而位图信息正是由 Canvas API 绘制的。实际上,Canvas 的绘制产物是 Picture 对象表示,而当前版本的 Flutter 中只有 PictureLayer 才拥有 picture 对象,换句话说,Flutter 中通过Canvas 绘制自身及其子节点的组件的绘制结果最终会落在 PictureLayer 中。
变换效果实现方式的选择
ContainerLayer 可以对其子 layer 整体进行一些变换,实际上,在大多数UI系统的 Canvas API 中也都有一些变换相关的 API ,那么也就意味着一些变换效果既可以通过 ContainerLayer 来实现,也可以通过 Canvas 来实现。比如,要实现平移变换,我们既可以使用 OffsetLayer ,也可以直接使用 Canva.translate API。既然如此,那我们选择实现方式的原则是什么呢?
容器类 Layer的变换在底层是通过 Skia 来实现的,不需要 Canvas 来处理。实现变换效果的具体原理是,有变换功能的容器类 Layer 会对应一个 Skia 引擎中的 Layer,为了和Flutter framework中 Layer 区分,flutter 中将 Skia 的Layer 称为 engine layer。而有变换功能的容器类 Layer 在添加到 Scene 之前就会构建一个 engine layer,我们以 OffsetLayer 为例,看看其相关实现:
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
// 构建 engine layer
engineLayer = builder.pushOffset(
layerOffset.dx + offset.dx,
layerOffset.dy + offset.dy,
oldLayer: _engineLayer as ui.OffsetEngineLayer?,
);
addChildrenToScene(builder);
builder.pop();
}
OffsetLayer 对其子节点整体做偏移变换的功能是 Skia 中实现支持的。Skia 可以支持多层渲染,但并不是层越多越好,engineLayer 是会占用一定的资源,Flutter 自带组件库中涉及到变换效果的都是优先使用 Canvas 来实现,如果 Canvas 实现起来非常困难或实现不了时才会用 ContainerLayer 来实现。
那么有什么场景下变换效果通过 Canvas 实现起来会非常困难,需要用 ContainerLayer 来实现 ?一个典型的场景是,我们需要对组件树中的某个子树整体做变换,且子树中的有多个 PictureLayer 时。这是因为一个 Canvas 往往对应一个 PictureLayer,不同 Canvas 之间相互隔离的,只有子树中所有组件都通过同一个 Canvas 绘制时才能通过该 Canvas 对所有子节点进行整体变换,否则就只能通过 ContainerLayer 。
/*
Canvas对象中也有名为 ...layer 相关的 API,如 Canvas.saveLayer,它和本节介绍的Layer 含义不同。Canvas对象中的 layer 主要是提供一种在绘制过程中缓存中间绘制结果的手段,为了在绘制复杂对象时方便多个绘制元素之间分离绘制而设计的
*/
组件树绘制流程
绘制相关实现在渲染对象 RenderObject,RenderObject 中和绘制相关的主要属性有:
1. layer
2. isRepaintBoundary(类型bool)
将isRepaintBoundary值为 true的RenderObject节点称为绘制边界节点
3. needsCompositing (类型bool)
Flutter 自带了一个 RepaintBoundary 组件,它的功能其实就是向组件树中插入一个绘制边界节点。
Flutter绘制组件树的大致流程(暂时忽略子树中需要“层合成”):
Flutter第一次绘制时,会从上到下开始递归的绘制子节点,每当遇到一个边界节点,则判断如果该边界节点的 layer 属性为空(类型为ContainerLayer),就会创建一个新的 OffsetLayer 并赋值给它;如果不为空,则直接使用它。然后会将边界节点的 layer 传递给子节点,接下来有两种情况:
1. 如果子节点是非边界节点,且需要绘制,则会在第一次绘制时:
1. 创建一个Canvas 对象和一个 PictureLayer,然后将它们绑定,后续调用Canvas 绘制都会落到和其绑定的PictureLayer 上。
2. 接着将这个 PictureLayer 加入到边界节点的 layer 中。
2. 如果不是第一次绘制,则复用已有的 PictureLayer 和 Canvas 对象 。
3. 如果子节点是边界节点,则对子节点递归上述过程。当子树的递归完成后,就要将子节点的layer 添加到父级 Layer中。
整个流程执行完后就生成了一棵Layer树。
1. RenderView 是 Flutter 应用的根节点,绘制会从它开始,因为他是一个绘制边界节点,在第一次绘制时,会为他创建一个 OffsetLayer,我们记为 OffsetLayer1,接下来 OffsetLayer1会传递给Row.
2. 由于 Row 是一个容器类组件且不需要绘制自身,那么接下来他会绘制自己的孩子,它有两个孩子,先绘制第一个孩子Column1,将 OffsetLayer1 传给 Column1,而 Column1 也不需要绘制自身,那么它又会将 OffsetLayer1 传递给第一个子节点Text1。
3. Text1 需要绘制文本,他会使用 OffsetLayer1进行绘制,由于 OffsetLayer1 是第一次绘制,所以会新建一个PictureLayer1和一个 Canvas1 ,然后将 Canvas1 和PictureLayer1 绑定,接下来文本内容通过 Canvas1 对象绘制,Text1 绘制完成后,Column1 又会将 OffsetLayer1 传给 Text2 。
4. Text2 也需要使用 OffsetLayer1 绘制文本,但是此时 OffsetLayer1 已经不是第一次绘制,所以会复用之前的 Canvas1 和 PictureLayer1,调用 Canvas1来绘制文本。
5. Column1 的子节点绘制完成后,PictureLayer1 上承载的是Text1 和 Text2 的绘制产物。
6. 接下来 Row 完成了 Column1 的绘制后,开始绘制第二个子节点 RepaintBoundary,Row 会将 OffsetLayer1 传递给 RepaintBoundary,由于它是一个绘制边界节点,且是第一次绘制,则会为它创建一个 OffsetLayer2,接下来 RepaintBoundary 会将 OffsetLayer2 传递给Column2,和 Column1 不同的是,Column2 会使用 OffsetLayer2 去绘制 Text3 和 Text4,绘制过程同Column1,在此不再赘述。
7. 当 RepaintBoundary 的子节点绘制完时,要将 RepaintBoundary 的 layer( OffsetLayer2 )添加到父级Layer(OffsetLayer1)中。
至此,整棵组件树绘制完成,生成了一棵右图所示的 Layer 树。需要说名的是 PictureLayer1 和 OffsetLayer2 是兄弟关系,它们都是 OffsetLayer1 的孩子。通过上面的例子我们至少可以发现一点:同一个 Layer 是可以多个组件共享的,比如 Text1 和 Text2 共享 PictureLayer1。
如果共享的话,会不会导致一个问题,比如 Text1 文本发生变化需要重绘时,是不是也会连带着 Text2 也必须重绘?
答案是:是!这貌似有点不合理,既然如此那为什么要共享呢?不能每一个组件都绘制在一个单独的 Layer 上吗?这样还能避免相互干扰。原因其实还是为了节省资源,Layer 太多时 Skia 会比较耗资源,所以这其实是一个trade-off。
再次强调一下,上面只是绘制的一般流程。一般情况下 Layer 树中的 ContainerLayer 和 PictureLayer 的数量和结构是和 Widget 树中的边界节点一一对应的,注意并不是和 Widget一一对应。 当然,如果 Widget 树中有子组件在绘制过程中添加了新的 Layer,那么Layer 会比边界节点数量多一些,这时就不是一一对应了。Flutter 中很多拥有变换、剪裁、透明等效果的组件的实现中都会往 Layer 树中添加新的 Layer。
发起重绘
RenderObject 是通过调用 markNeedsRepaint 来发起重绘请求的。
绘制过程存在Layer共享,所以重绘时 需要重绘所有共享同一个Layer的组件。当一个节点需要重绘时,我们得找到离它最近的第一个父级绘制边界节点,然后让它重绘即可。当一个节点调用了它时,markNeedsRepaint正是完成了这个过程。具体的步骤如下:
1. 会从当前节点一直往父级查找,直到找到一个绘制边界节点时终止查找,然后会将该绘制边界节点添加到其PiplineOwner的 _nodesNeedingPaint列表中(保存需要重绘的绘制边界节点)。
2. 在查找的过程中,会将自己到绘制边界节点路径上所有节点的_needsPaint属性置为true,表示需要重新绘制。
3. 请求新的 frame ,执行重绘重绘流程。
markNeedsRepaint 删减后的核心源码如下:
void markNeedsPaint() {
if (_needsPaint) return;
_needsPaint = true;
if (isRepaintBoundary) { // 如果是当前节点是边界节点
owner!._nodesNeedingPaint.add(this); // 将当前节点添加到需要重新绘制的列表中。
owner!.requestVisualUpdate(); // 请求新的frame,该方法最终会调用scheduleFrame()
} else if (parent is RenderObject) { // 若不是边界节点且存在父节点
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint(); // 递归调用父节点的markNeedsPaint
} else {
// 如果是根节点,直接请求新的 frame 即可
if (owner != null)
owner!.requestVisualUpdate();
}
}
在当前版本的Flutter中是永远不会走到最后一个else分支的,因为当前版本中根节点是一个RenderView,而该组件的isRepaintBoundary 属性为 true,所以如果调用 renderView.markNeedsPaint()是会走到isRepaintBoundary为 true的分支的。
请求新的 frame后,下一个 frame 到来时就会走drawFrame流程,drawFrame中和绘制相关的涉及flushCompositingBits、flushPaint 和 compositeFrame 三个函数,而重新绘制的流程在 flushPaint 中,所以我们先重点看一下flushPaint的流程。
flushPaint流程
遍历需要绘制的节点列表,然后逐个开始绘制。
final List<RenderObject> dirtyNodes = nodesNeedingPaint;
for (final RenderObject node in dirtyNodes){
PaintingContext.repaintCompositedChild(node);
}
组件树中某个节点要更新自己时会调用markNeedsRepaint方法,而该方法会从当前节点一直往上查找,直到找到一个isRepaintBoundary为 true 的节点,然后会将该节点添加到 nodesNeedingPaint列表中。因此,nodesNeedingPaint中的节点的isRepaintBoundary 必然为 true,换句话说,能被添加到 nodesNeedingPaint列表中节点都是绘制边界,那么这个边界究竟是如何起作用的,我们继续看PaintingContext.repaintCompositedChild 函数的实现。
static void repaintCompositedChild( RenderObject child, PaintingContext? childContext) {
assert(child.isRepaintBoundary); // 断言:能走的这节点,其isRepaintBoundary必定为true.
OffsetLayer? childLayer = child.layer;
if (childLayer == null) { //如果边界节点没有layer,则为其创建一个OffsetLayer
final OffsetLayer layer = OffsetLayer();
child.layer = childLayer = layer;
} else { //如果边界节点已经有layer了(之前绘制时已经为其创建过layer了),则清空其子节点。
childLayer.removeAllChildren();
}
//通过其layer构建一个paintingContext,之后layer便和childContext绑定,这意味着通过同一个
//paintingContext的canvas绘制的产物属于同一个layer。
paintingContext ??= PaintingContext(childLayer, child.paintBounds);
//调用节点的paint方法,绘制子节点(树)
child.paint(paintingContext, Offset.zero);
childContext.stopRecordingIfNeeded();//这行后面解释
}
可以看到,在绘制边界节点时会首先检查其是否有layer,如果没有就会创建一个新的 OffsetLayer 给它,随后会根据该 offsetLayer 构建一个 PaintingContext 对象(记为context),之后子组件在获取context的canvas对象时会创建一个 PictureLayer,然后再创建一个 Canvas 对象和新创建的 PictureLayer 关联起来,这意味着后续通过同一个paintingContext 的 canvas 绘制的产物属于同一个PictureLayer。下面我们看看相关源码:
Canvas get canvas {
//如果canvas为空,则是第一次获取;
if (_canvas == null) _startRecording();
return _canvas!;
}
//创建PictureLayer和canvas
void _startRecording() {
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
//将pictureLayer添加到_containerLayer(是绘制边界节点的Layer)中
_containerLayer.append(_currentLayer!);
}
下面我们再来看看 child.paint 方法的实现,该方法需要节点自己实现,用于绘制自身,节点类型不同,绘制算法一般也不同,不过功能是差不多的,即:如果是容器组件,要绘制孩子和自身(也可能自身也可能没有绘制逻辑,只绘制孩子,比如Center组件),如果不是容器类组件,则绘制自己(比如Image)。
void paint(PaintingContext context, Offset offset) {
// ...自身的绘制
if(hasChild){ //如果该组件是容器组件,绘制子节点。
context.paintChild(child, offset)
}
//...自身的绘制
}
接下来我们看一下context.paintChild方法:它的主要逻辑是:如果当前节点是边界节点且需要重新绘制,则先调用上面解析过的repaintCompositedChild方法,该方法执行完毕后,会将当前节点的layer添加到父边界节点的Layer中;如果当前节点不是边界节点,则调用paint方法(上面刚说过):
//绘制孩子
void paintChild(RenderObject child, Offset offset) {
//如果子节点是边界节点,则递归调用repaintCompositedChild
if (child.isRepaintBoundary) {
if (child._needsPaint) { //需要重绘时再重绘
repaintCompositedChild(child);
}
//将孩子节点的layer添加到Layer树中,
final OffsetLayer childOffsetLayer = child.layer! as OffsetLayer;
childOffsetLayer.offset = offset;
//将当前边界节点的layer添加到父边界节点的layer中.
appendLayer(childOffsetLayer);
} else {
// 如果不是边界节点直接绘制自己
child.paint(this, offset);
}
}
这里需要注意三点:
1. 绘制孩子节点时,如果遇到边界节点且当其不需要重绘(_needsPaint 为 false) 时,会直接复用该边界节点的 layer,而无需重绘!这就是边界节点能跨 frame 复用的原理。
2. 因为边界节点的layer类型是ContainerLayer,所以是可以给它添加子节点。
3. 注意是将当前边界节点的layer添加到 父边界节点,而不是父节点。
按照上面的流程执行完毕后,最终所有边界节点的layer就会相连起来组成一棵Layer树。
创建新的 PictureLayer
在上面示例中给Row添加第三个子节点 Text5。
因为 Text5 是在 RepaintBoundary 绘制完成后才会绘制,上例中当 RepaintBoundary 的子节点绘制完时,将 RepaintBoundary 的 layer( OffsetLayer2 )添加到父级Layer(OffsetLayer1)中后发生了什么?答案在我们上面介绍的repaintCompositedChild 的最后一行:
childContext.stopRecordingIfNeeded();
我们看看其删减后的核心代码:
void stopRecordingIfNeeded() {
_currentLayer!.picture = _recorder!.endRecording();// 将canvas绘制产物保存在 PictureLayer中
_currentLayer = null;
_recorder = null;
_canvas = null;
}
当绘制完 RepaintBoundary 走到 childContext.stopRecordingIfNeeded() 时, childContext 对应的 Layer 是 OffsetLayer1,而 _currentLayer 是 PictureLayer1, _canvas 对应的是 Canvas1。我们看到实现很简单,先将 Canvas1 的绘制产物保存在 PictureLayer1 中,然后将一些变量都置空。
接下来再绘制 Text5 时,要先通过context.canvas 来绘制,根据 canvas getter的实现源码,此时就会走到 _startRecording() 方法,该方法我们上面介绍过,它会重新生成一个 PictureLayer 和一个新的 Canvas :
Canvas get canvas {
//如果canvas为空,则是第一次获取;
if (_canvas == null) _startRecording();
return _canvas!;
}
之后,我们将新生成的 PictureLayer 和 Canvas 记为 PictureLayer3 和 Canvas3,Text5 的绘制会落在 PictureLayer3 上
总结一下:
父节点在绘制子节点时,如果子节点是绘制边界节点,则在绘制完子节点后会生成一个新的 PictureLayer,后续其他子节点会在新的 PictureLayer 上绘制。
为什么要这么做呢?直接复用之前的 PictureLayer1 有问题吗?答案:在当前的示例中是不会有问题,但是在层叠布局的场景中就会有问题。
左边是一个 Stack 布局,右边是对应的Layer树结构;我们知道Stack布局中会根据其子组件的加入顺序进行层叠绘制,最先加入的孩子在最底层,最后加入的孩子在最上层。可以设想一下如果绘制 Child3 时复用了 PictureLayer1,则会导致 Child3 被 Child2 遮住,这显然不符合预期,但如果新建一个 PictureLayer 在添加到 OffsetLayer 最后面,则可以获得正确的结果。
现在我们再来深入思考一下:如果 Child2 的父节点不是 RepaintBoundary,那么是否就意味着 Child3 和 Child1就可以共享同一个 PictureLayer 了?
答案是否定的!如果 Child2 的父组件改为一个自定义的组件,在这个自定义的组件中我们希望对子节点在渲染时进行一些举证变化,为了实现这个功能,我们创建一个新的 TransformLayer 并指定变换规则,然后我们把它传递给 Child2,Child2会绘制完成后,我们需要将 TransformLayer 添加到 Layer 树中(不添加到Layer树中是不会显示的)
可以发现这种情况本质上和上面使用 RepaintBoudary 的情况是一样的,Child3 仍然不应该复用 PictureLayer1,那么现在我们可以总结一个一般规律了:只要一个组件需要往 Layer 树中添加新的 Layer,那么就必须也要结束掉当前 PictureLayer 的绘制。这也是为什么 PaintingContext 中需要往 Layer 树中添加新 Layer 的方法(比如pushLayer、addLayer)中都有如下两行代码:
stopRecordingIfNeeded(); //先结束当前 PictureLayer 的绘制
appendLayer(layer);// 再添加到 layer树
这是向 Layer 树中添加Layer的标准操作。这个结论要牢记,我们在后面介绍 flushCompositingBits() 的原理时会用到。
compositeFrame
创建好layer后,接下来就需要上屏展示了,而这部分工作是由renderView.compositeFrame方法来完成的。实际上他的实现逻辑很简单:先通过layer构建Scene,最后再通过window.render API 来渲染:
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer!.buildScene(builder);
window.render(scene);
值得一提的是构建Scene的过程,我们看一下核心源码:
ui.Scene buildScene(ui.SceneBuilder builder) {
updateSubtreeNeedsAddToScene();
addToScene(builder); //关键
final ui.Scene scene = builder.build();
return scene;
}
最关键的一行就是调用addToScene,该方法主要的功能就是将Layer树中每一个layer传给Skia(最终会调用native API,如果想了解详情,建议查看 OffsetLayer 和 PictureLayer 的 addToScene 方法),这是上屏前的最后一个准备动作,最后就是调用 window.render 将绘制数据发给GPU,渲染出来了!