上一篇文章我们讲了Flutter的三棵树是如何创建并建立联系的,有了这三棵树,本篇文章将分析布局和绘制的流程。
1.布局与绘制入口
我们摘抄一段《Flutter实战·第二版》的原文:
一次绘制过程,我们称其为一帧(frame)。我们之前说的 Flutter 可以实现60fps(Frame Per-Second)就是指一秒钟最多可以触发 60 次重绘,FPS 值越大,界面就越流畅。这里需要说明的是 Flutter中 的 frame 概念并不等同于屏幕刷新帧(frame),因为Flutter UI 框架的 frame 并不是每次屏幕刷新都会触发,这是因为,如果 UI 在一段时间不变,那么每次屏幕刷新都重新走一遍渲染流程是不必要的,因此,Flutter 在第一帧渲染结束后会采取一种主动请求 frame 的方式来实现只有当UI可能会改变时才会重新走渲染流程。
由此我们可知,Flutter其实并不是完全根据屏幕刷新频率来发送重绘信号进行绘制的。事实上,通常我们在实际中进行绘制的主要有两种场景:
- App启动,即
runApp()
。 - 数据变化时发起重绘,比如:
setState()
。
1.1.runApp()
我们先回顾上一篇文章说到的启动流程:程序入口runApp()
的实现:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
在这个过程中创建了Flutter
的三棵树,并将三棵树进行绑定。最终调用scheduleWarmUpFrame()
触发首帧的绘制。scheduleWarmUpFrame()
会执行到handleDrawFrame()
方法,核心实现是在RendererBinding
的drawFrame()
方法中:
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
即真正的每一帧绘制过程,这个过程我们之后分析。由此我们可知在启动App的时候,不发送Vsync
信号即可触发首帧的绘制。
1.2.setState()
另一个常用场景就是当数据发生变化时,我们调用setState()
进行更新重绘。我们看一下setState()
的实现:
@protected
void setState(VoidCallback fn) {
//...
_element!.markNeedsBuild();
}
核心实现就是_element!.markNeedsBuild()
。
void markNeedsBuild() {
assert(_lifecycleState != _ElementLifecycle.defunct);
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
//...
if (dirty) {
return;
}
_dirty = true;
owner!.scheduleBuildFor(this);
}
将Widget
所对应的Element
标记为dirty
。这个owner
就是我们上一篇文章中提到的在WidgetsBinding
初始化时创建的BuildOwner
对象,我们回顾一下它的创建:
@override
void initInstances() {
super.initInstances();
//...
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;
//...
}
buildOwner
持有个onBuildScheduled
的callBack
,它的实现是:
void _handleBuildScheduled() {
//...
ensureVisualUpdate();
}
其中ensureVisualUpdate()
是SchedulerBinding
的方法,其实现如下:
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
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 scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
实际上就是请求新的frame
。那么onBuildScheduled
是什么时候被调用的呢?回顾完BuildOwner
我们再回到setState()
,看看owner!.scheduleBuildFor(this)
的实现:
void scheduleBuildFor(Element element) {
//...
if (element._inDirtyList) {
//...
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
_dirtyElements.add(element);
element._inDirtyList = true;
//...
}
我们看到了一个熟悉的方法执行:BuildOwner
的onBuildScheduled
被触发了。也就是请求了下一帧的frame
等待被重绘,再把Element
添加到dirty element
列表中等待下一帧的rebuild
。
setState()
的流程也分析完了,我们总结一下:在启动App的时候,不会发送Vsync
信号,直接进行首帧的绘制。而在数据发生变化需要重绘,比如 setState()
被触发时,会将Widget
对应的Element
标记为dirty
,然后发送Vsync
信号等待被重绘。
在分析具体的绘制流程之前,我们分析一下发送Vsync
信号后是如何触发重绘流程的。
2.SchedulerBinding
我们回到WidgetsFlutterBinding
,上一篇文章介绍过,WidgetsFlutterBinding
混合了各种Binding
。这些Binding
基本都是监听并处理Window
对象的一些事件,然后将这些事件按照Framework的
模型包装、抽象然后分发。其中SchedulerBinding
监听刷新事件,绑定Framework
绘制调度。
我们回到上一章节提到的scheduleFrame()
的实现,它是SchedulerBinding
的方法:
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) {
return;
}
assert(() {
if (debugPrintScheduleFrameStacks) {
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
}
return true;
}());
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
在执行platformDispatcher.scheduleFrame()
发起重绘之前调用了ensureFrameCallbacksRegistered()
:
@protected
void ensureFrameCallbacksRegistered() {
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
为platformDispatcher
注册了onBeginFrame
和onDrawFrame
回调。我们追踪一下onBeginFrame
和onDrawFrame
的调用时机:
FrameCallback? get onBeginFrame => _onBeginFrame;
set onBeginFrame(FrameCallback? callback) {
_onBeginFrame = callback;
_onBeginFrameZone = Zone.current;
}
VoidCallback? get onDrawFrame => _onDrawFrame;
set onDrawFrame(VoidCallback? callback) {
_onDrawFrame = callback;
_onDrawFrameZone = Zone.current;
}
其get
方法是在SingletonFlutterWindow
中被执行的:
FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;
VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;
SingletonFlutterWindow
中onBeginFrame
和onDrawFrame
就是当Engine
层收到Vsync
信号时被回调的,也就是说发送了重绘信号后,将会在此时被执行。我们回到SchedulerBinding
的ensureFrameCallbacksRegistered()
方法:
@protected
void ensureFrameCallbacksRegistered() {
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
追踪_handleBeginFrame
和_handleDrawFrame
的实现:
_handleBeginFrame
:
void _handleBeginFrame(Duration rawTimeStamp) {
if (_warmUpFrame) {
//...
_rescheduleAfterWarmUpFrame = true;
return;
}
handleBeginFrame(rawTimeStamp);
}
void handleBeginFrame(Duration? rawTimeStamp) {
_frameTimelineTask?.start('Frame');
_firstRawTimeStampInEpoch ??= rawTimeStamp;
_currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
if (rawTimeStamp != null) {
_lastRawTimeStamp = rawTimeStamp;
}
//...
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
_frameTimelineTask?.start('Animate');
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id)) {
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
}
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
首先记录了一下绘制初始的时间戳,然后遍历_transientCallbacks
逐一进行回调。_transientCallbacks
是执行“临时”回调任务,”临时“回调任务只能被执行一次,执行后会被移出”临时“任务队列。典型的代表就是动画回调会在该阶段执行。
_handleDrawFrame
:
void _handleDrawFrame() {
//...
handleDrawFrame();
}
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
_frameTimelineTask?.finish(); // end the "Animate" phase
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks) {
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
}
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.of(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks) {
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
}
} finally {
//...
}
}
它遍历了persistentCallback
和postFrameCallbacks
,并逐一执行。其中persistentCallback
执行一些持久的任务(每一个frame都要执行的任务),比如渲染管线(构建、布局、绘制)就是在该任务队列中执行的。postFrameCallbacks
在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和请求新的frame。由此可知persistentCallback
就是具体布局绘制的地方,那么persistentCallback
是在哪里注册的呢?我们回顾一下上一篇文章的RendererBinding
的初始化:
@override
void initInstances() {
super.initInstances();
_instance = this;
_pipelineOwner = PipelineOwner(
onNeedVisualUpdate: ensureVisualUpdate,
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
platformDispatcher
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
initRenderView();
_handleSemanticsEnabledChanged();
assert(renderView != null);
addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker();
if (kIsWeb) {
addPostFrameCallback(_handleWebFirstFrame);
}
}
在initInstances()
时会调用addPersistentFrameCallback(_handlePersistentFrameCallback)
方法进行注册,而_handlePersistentFrameCallback
的实现如下:
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}
执行了drawFrame()
:
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
好到此为止当数据发生变化,比如执行setState()
时发送Vsync
信号请求重绘也调到了drawFrame()
方法。真正的布局和绘制是由pipelineOwner
这个渲染管道进行处理的。接下来的章节才要开始我们的正题:Flutter的布局和绘制流程。
3.flushLayout()
flushLayout
确定每一个组件的布局信息(大小和位置)。我们先来看它的实现:
void flushLayout() {
if (!kReleaseMode) {
Map<String, String>? debugTimelineArguments;
//...
try {
while (_nodesNeedingLayout.isNotEmpty) {
assert(!_shouldMergeDirtyNodes);
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (int i = 0; i < dirtyNodes.length; i++) {
if (_shouldMergeDirtyNodes) {
_shouldMergeDirtyNodes = false;
if (_nodesNeedingLayout.isNotEmpty) {
_nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));
break;
}
}
final RenderObject node = dirtyNodes[I];
if (node._needsLayout && node.owner == this) {
node._layoutWithoutResize();
}
}
_shouldMergeDirtyNodes = false;
}
} finally {
//...
}
}
_nodesNeedingLayout
是RenderObject
列表,表示需要被重新布局的节点。遍历_nodesNeedingLayout
,并执行node._layoutWithoutResize()
方法。
那么这个_nodesNeedingLayout
是如何生成的呢?
通过跟踪代码,我们发现时RenderObject
调用其markNeedsLayout()
方法为_nodesNeedingLayout
添加数据的:
void markNeedsLayout() {
//...
if (_relayoutBoundary == null) {
_needsLayout = true;
if (parent != null) {
markParentNeedsLayout();
}
return;
}
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
//...
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();
}
}
}
这段代码的目的是通过找到_relayoutBoundary
来确定需要重新被布局的节点。如果自己已经是_relayoutBoundary
,那么执行owner!._nodesNeedingLayout.add(this)
将自己添加到_nodesNeedingLayout
中;否则只要有parent,那么就调用markParentNeedsLayout()
到父节点去寻找_relayoutBoundary
:
@protected
void markParentNeedsLayout() {
assert(_debugCanPerformMutations);
_needsLayout = true;
assert(this.parent != null);
final RenderObject parent = this.parent! as RenderObject;
if (!_doingThisLayoutWithCallback) {
parent.markNeedsLayout();
} else {
assert(parent._debugDoingThisLayout);
}
assert(parent == this.parent);
}
这是个递归的过程,parent会调用parent.markNeedsLayout()
,目的就是找到_relayoutBoundary
添加到_nodesNeedingLayout
。
那么这个_relayoutBoundary
是如何确定的呢?之后再进行说明。
另外markNeedsLayout()
还有什么调用时机呢?通过追踪,在RenderObject
的attach()
方法会执行markNeedsLayout()
@override
void attach(PipelineOwner owner) {
assert(!_debugDisposed);
super.attach(owner);
if (_needsLayout && _relayoutBoundary != null) {
_needsLayout = false;
markNeedsLayout();
}
if (_needsCompositingBitsUpdate) {
_needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate();
}
if (_needsPaint && _layerHandle.layer != null) {
_needsPaint = false;
markNeedsPaint();
}
if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
_needsSemanticsUpdate = false;
markNeedsSemanticsUpdate();
}
}
我们回到flushLayout()
,看看node._layoutWithoutResize()
的实现:
void _layoutWithoutResize() {
assert(_relayoutBoundary == this);
RenderObject? debugPreviousActiveLayout;
//...
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
//...
_needsLayout = false;
markNeedsPaint();
}
主要执行了两个方法:performLayout()
和markNeedsPaint()
。在布局中最重要的实现就是performLayout()
的实现。我们先来看performLayout()
,它是RenderObject
的一个抽象方法,需要子类来实现。
performLayout()
需要做这么几件事:
- 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
- 子节点根据约束信息确定自己的大小(size)。
- 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
- 递归整个过程,确定出每一个节点的大小和位置。
我们看个例子,比如说Align
这个控件。Align
继承SingleChildRenderObjectWidget
,它开放了createRenderObject()
方法由子类实现,返回一个RenderObject
对象。我们看看Align
的实现:
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
RenderPositionedBox
的performLayout()
实现如下:
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
if (child != null) {
child!.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
alignChild();
} else {
//...
}
}
@protected
void alignChild() {
_resolve();
//...
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}
只考虑Align
有child
的情况:
-
constraints
即约束条件,限制子节点的最大和最小宽高。 - 先对
child
进行layout()
,并获取child
的size
。 - 根据
child
的大小确定自身的大小size
。 - 执行
alignChild()
算出child
在Align
组件中的偏移,然后将这个偏移保存在child
的parentData
中,parentData
在绘制阶段会用到。
我们分析一下对child
进行layout()
,layout()
的实现:
void layout(Constraints constraints, { bool parentUsesSize = false }) {
if (!kReleaseMode && debugProfileLayoutsEnabled) {
Map<String, String> debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent;
//...
final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
//...
if (!_needsLayout && constraints == _constraints) {
//...
if (relayoutBoundary != _relayoutBoundary) {
_relayoutBoundary = relayoutBoundary;
visitChildren(_propagateRelayoutBoundaryToChild);
}
if (!kReleaseMode && debugProfileLayoutsEnabled)
Timeline.finishSync();
return;
}
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
//...
if (sizedByParent) {
//...
try {
performResize();
//...
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
//...
}
RenderObject? debugPreviousActiveLayout;
//...
try {
performLayout();
markNeedsSemanticsUpdate();
//...
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
//...
_needsLayout = false;
markNeedsPaint();
if (!kReleaseMode && debugProfileLayoutsEnabled)
Timeline.finishSync();
}
这段代码非常核心,首先就确定了之前我们遗留的relayoutBoundary
的疑问:
final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
relayoutBoundary
确定的原则是“组件自身的大小变化不会影响父组件”,如果一个组件满足以下四种情况之一,则它便是relayoutBoundary
:
- 非
parentUsesSize
,即当前组件父组件的大小不依赖当前组件大小时。 -
sizedByParent
,组件的大小只取决于父组件传递的约束,而不会依赖子组件的大小。 -
constraints.isTight
,父组件传递给自身的约束是一个严格约束。这种情况下即使自身的大小依赖后代元素,但也不会影响父组件。 -
parent is! RenderObject
,说明当前组件是根组件,即RenderView
,它没有parent。
如果当前组件不是relayoutBoundary
,那么就把父组件的_relayoutBoundary
赋值给自己的relayoutBoundary
。
之后如果_needsLayout
为false
,且constraints
约束条件没有变化,则无需重新布局直接return
。
将relayoutBoundary
赋值给自己的成员变量_relayoutBoundary
。
如果是sizedByParent
的,则执行performResize()
方法。performResize()
是个抽象方法,需要子类进行实现,我们再摘抄一段《Flutter实战·第二版》的原文:
performLayout
中确定当前组件的大小时通常会依赖子组件的大小,如果sizedByParent
为true
,则当前组件的大小就不依赖子组件大小了,为了逻辑清晰,Flutter 框架中约定,当sizedByParent
为true
时,确定当前组件大小的逻辑应抽离到performResize()
中,这种情况下performLayout
主要的任务便只有两个:对子组件进行布局和确定子组件在当前组件中的布局起始位置偏移。
回到layout()
方法,执行performLayout()
,performLayout()
又回调用child
的layout()
方法形成递归。之后将_needsLayout
设置为false,然后调用markNeedsPaint()
请求重绘:
void markNeedsPaint() {
//...
if (_needsPaint) {
return;
}
_needsPaint = true;
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if (isRepaintBoundary && _wasRepaintBoundary) {
//...
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
//...
if (owner != null) {
owner!.requestVisualUpdate();
}
}
}
大概的逻辑是如果自己是isRepaintBoundary
,那么将自己添加到_nodesNeedingPaint
列表中,_nodesNeedingPaint
将在绘制的时候使用;否则如果有父的话调用父的markNeedsPaint()
形成递归。
到此为止,flushLayout()
到实现就分析完毕了。我们继续分析flushCompositingBits()
。
4.flushCompositingBits()
4.1.Layer
Layer
需要拿出来单独写一篇文章去讲,在此只是介绍一下,以便更好的理解绘制过程。
Flutter的绘制会将RenderObject
树转成Layer
树,Layer
的组成由上一章节提到的RenderObject
中的isRepaintBoundary
来决定。
Layer
有两种类型。
- 容器类
Layer
:最常用的是OffsetLayer
,通常情况下一个repaintBoundary
就对应一个OffsetLayer
。比方说每一个路由都封装了一个repaintBoundary
,那么每一个路由就在一个单独的Layer
里。另外容器类Layer
可以对其子Layer
整体做一些变换效果,比如剪裁效果(ClipRectLayer
、ClipRRectLayer
、ClipPathLayer
)、过滤效果(ColorFilterLayer
、ImageFilterLayer
)、矩阵变换(TransformLayer
)、透明变换(OpacityLayer
)等。 - 绘制类
Layer
:最常用的是PictureLayer
,比方说我们常用的Text
,Image
组件等就在PictureLayer
里。另外还有TextureLayer
和PlatformViewLayer
主要用于native
相关的绘制。
Layer
的作用一个是为了在不同的frame
下复用绘制的产物,另外就是确定绘制边界缩小绘制的区域。
关于Layer
在此就不再做展开了,有空的时候会再写一篇认真分析一下Layer
。我们回到flushCompositingBits()
继续分析。
4.2.flushCompositingBits()
void flushCompositingBits() {
if (!kReleaseMode) {
Timeline.startSync('UPDATING COMPOSITING BITS');
}
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this) {
node._updateCompositingBits();
}
}
_nodesNeedingCompositingBitsUpdate.clear();
if (!kReleaseMode) {
Timeline.finishSync();
}
}
遍历_nodesNeedingCompositingBitsUpdate
列表,并执行_updateCompositingBits()
方法。它的作用是标记需要被合成的Layer
所对应的对象。
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate) {
return;
}
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing) {
_needsCompositing = true;
}
});
if (isRepaintBoundary || alwaysNeedsCompositing) {
_needsCompositing = true;
}
// If a node was previously a repaint boundary, but no longer is one, then
// regardless of its compositing state we need to find a new parent to
// paint from. To do this, we mark it clean again so that the traversal
// in markNeedsPaint is not short-circuited. It is removed from _nodesNeedingPaint
// so that we do not attempt to paint from it after locating a parent.
if (!isRepaintBoundary && _wasRepaintBoundary) {
_needsPaint = false;
_needsCompositedLayerUpdate = false;
owner?._nodesNeedingPaint.remove(this);
_needsCompositingBitsUpdate = false;
markNeedsPaint();
} else if (oldNeedsCompositing != _needsCompositing) {
_needsCompositingBitsUpdate = false;
markNeedsPaint();
} else {
_needsCompositingBitsUpdate = false;
}
}
递归遍历子树确定如果每一个节点的_needsCompositing
值,在绘制时只需要判断一下当前的needsCompositing
就能知道子树是否存在待合成的layer
了。那么什么情况下Layer
要被合成呢?我们回过头看一下_nodesNeedingCompositingBitsUpdate
是怎么来的。
void markNeedsCompositingBitsUpdate() {
assert(!_debugDisposed);
if (_needsCompositingBitsUpdate) {
return;
}
_needsCompositingBitsUpdate = true;
if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
if (parent._needsCompositingBitsUpdate) {
return;
}
if ((!_wasRepaintBoundary || !isRepaintBoundary) && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
// parent is fine (or there isn't one), but we are dirty
if (owner != null) {
owner!._nodesNeedingCompositingBitsUpdate.add(this);
}
}
调用markNeedsCompositingBitsUpdate()
的主要场景是当子节点会向Layer
树中添加新的绘制类Layer
时,则父级的变换类组件中就需要合成Layer
。这个怎么理解呢?我们还是摘抄一个《Flutter实战·第二版》的实例:
@override
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: RepaintBoundary( // 添加一个 RepaintBoundary
child: Text(
"A",
textScaleFactor: 5,
),
),
),
);
}
其中CustomRotatedBox
这个控件的作用是将child
旋转90度。它的Layer
结构如下图所示:
执行上面的代码,会发现
Text("A")
并没有旋转90度,原因是CustomRotatedBox
中进行旋转变换的canvas
对应的是PictureLayer1
,而 Text("A")
的绘制是使用的PictureLayer2
对应的canvas
,他们属于不同的 Layer
。可以发现父子的PictureLayer
"分离了",所以CustomRotatedBox
也就不会对Text("A")
起作用。解决这个问题的方法是:
- 创建一个
TransformLayer
(记为TransformLayer1
) 添加到Layer
树中,接着创建一个新的PaintingContext
和TransformLayer1
绑定。 - 子节点通过这个新的
PaintingContext
去绘制。
如下图所示:
这其实就是一个重新
Layer
合成的过程:创建一个新的ContainerLayer
,然后将该ContainerLayer
传递给子节点,这样后代节点的Layer
必然属于ContainerLayer
,那么给这个ContainerLayer
做变换就会对其全部的子孙节点生效。回到我们之前的代码,所以在
flushCompositingBits()
过程中其实就是给这些TransformLayer
去打_needsCompositing
标记的,为之后的Layer
合成做准备。到此为止,
flushCompositingBits()
到实现就分析完毕了。我们继续分析flushPaint()
。
5.flushPaint()
代码如下:
void flushPaint() {
if (!kReleaseMode) {
Map<String, String>? debugTimelineArguments;
//...
}
try {
//...
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layerHandle.layer != null);
if ((node._needsPaint || node._needsCompositedLayerUpdate) && node.owner == this) {
if (node._layerHandle.layer!.attached) {
assert(node.isRepaintBoundary);
if (node._needsPaint) {
PaintingContext.repaintCompositedChild(node);
} else {
PaintingContext.updateLayerProperties(node);
}
} else {
node._skippedPaintingOnLayer();
}
}
}
assert(_nodesNeedingPaint.isEmpty);
} finally {
//...
}
}
遍历_nodesNeedingPaint
列表,逐一执行PaintingContext.repaintCompositedChild(node)
方法:
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
assert(child._needsPaint);
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
//...
OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;
if (childLayer == null) {
//...
final OffsetLayer layer = child.updateCompositedLayer(oldLayer: null);
child._layerHandle.layer = childLayer = layer;
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
Offset? debugOldOffset;
//...
childLayer.removeAllChildren();
final OffsetLayer updatedLayer = child.updateCompositedLayer(oldLayer: childLayer);
//...
}
child._needsCompositedLayerUpdate = false;
//...
childContext ??= PaintingContext(childLayer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
//...
childContext.stopRecordingIfNeeded();
}
由于被重绘的部分一定是repaintBoundary
,所以会持有一个OffsetLayer
,再创建一个PaintingContext
对象,之后调用child._paintWithContext(childContext, Offset.zero)
进行处理:
void _paintWithContext(PaintingContext context, Offset offset) {
//...
if (_needsLayout) {
return;
}
if (!kReleaseMode && debugProfilePaintsEnabled) {
Map<String, String>? debugTimelineArguments;
//...
RenderObject? debugLastActivePaint;
//...
_needsPaint = false;
_needsCompositedLayerUpdate = false;
_wasRepaintBoundary = isRepaintBoundary;
try {
paint(context, offset);
//...
} catch (e, stack) {
_debugReportException('paint', e, stack);
}
//...
}
重要的paint()
来了,paint()
是个抽象方法,由子类进行实现。paint()
的实现思路通常是这样的:如果是容器组件,要绘制自己和child
,如果不是容器类组件,则绘制自己(比如Image)。自身的绘制就是在context.canvas
上进行绘制。
void paint(PaintingContext context, Offset offset) {
// ...自身的绘制
if(hasChild){ //如果该组件是容器组件,绘制子节点。
context.paintChild(child, offset)
}
//...自身的绘制
}
我们看看context.paintChild(child, offset)
的实现:
void paintChild(RenderObject child, Offset offset) {
//...
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else if (child._wasRepaintBoundary) {
assert(child._layerHandle.layer is OffsetLayer);
child._layerHandle.layer = null;
child._paintWithContext(this, offset);
} else {
child._paintWithContext(this, offset);
}
}
先判断child
是不是repaintBoundary
,如果是且需要被合成的话,那么先将child
的Layer
进行合成,否则的话,递归执行child._paintWithContext(this, offset)
,进行绘制。
flushPaint()
就梳理完成了,还差最后一个方法:renderView.compositeFrame()
。
5.compositeFrame()
先看代码:
void compositeFrame() {
if (!kReleaseMode) {
Timeline.startSync('COMPOSITING');
}
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer!.buildScene(builder);
if (automaticSystemUiAdjustment) {
_updateSystemChrome();
}
_window.render(scene);
scene.dispose();
//...
} finally {
if (!kReleaseMode) {
Timeline.finishSync();
}
}
}
它的逻辑是:先通过Layer
构建Scene
,最后再通过window.render API
来渲染。这个过程我们就不分析了,总而言之就是将Layer
树中每一个Layer
传给Skia
进行绘制。
6.总结
通过《Flutter的布局和绘制详解 ---三棵树》和《Flutter的布局和绘制详解 ---布局和绘制》这两篇文章的梳理,我们知道了Flutter的页面是如何构建起来,并进行布局和绘制的,也了解了是通过什么方式节约资源的。希望能对大家有所帮助。