本文所有源码版本为Flutter 1.9.1,部分源码会删除assert和debug部分
转载请注明出处,谢谢
0、本系列文章
- 深入Flutter的Rendering层(一)--- 从runApp到三棵树的构建
- 深入Flutter的Rendering层(二)--- 布局layout与绘制paint
1、前言
上一篇文章,我们从runApp切入,分析了Flutter三棵树的构建过程,也深入了解了里面的运行机制。本文将着重分析帧绘制(drawFrame)的执行过程。其中有部分概念和逻辑会跟上一篇文章有关联。
2、渲染原理
- 首先Flutter的Engine(C++)层会通过ScheduleFrame()来注册Vsync信号回调;而Engine(C++)层与Framework(Dart)层是经过Window类进行沟通(关于Window类上一篇有说明);
- 当Engine层收到Vsync信号,它会通过Window的onBeginFrame和onDrawFrame方法进行回调,此时信号已经回调到Framework层;
- Framework层会把我们的Widget通过build、layout、paint等处理,把最终的绘制内容画在Layer上;
- 然后Framework层通过Window的render方法回到了Engine层,Engine再向GPU线程提交绘制任务;最终渲染出来。
其中第3步就是本系列文章着重分析的内容。这一步还可以再细分为:
- 将我们的Widget构建出对应的Element树和RenderObject树;(这一步在上一篇文章分析了)
- 将RenderObject进行layout和paint,把绘制的结果输出在Layer上;这个步骤就是本文的核心分析内容;
3、Framework层绘制
Flutter Engine收到Vsync信号会依次调用Window类的onBeginFrame和onDrawFrame;这两个接口在Framework层有对应的注册。
3.1 SchedulerBinding.initInstances
[源码路径:flutter/lib/src/scheduler/binding.dart]
mixin SchedulerBinding on BindingBase, ServicesBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
window.onBeginFrame = _handleBeginFrame;
window.onDrawFrame = _handleDrawFrame;
...
};
}
}
这个函数会在runApp的第一步WidgetsFlutterBinding.ensureInitialized()调用。可以看到收到Vsync信号后,会回调到_handleBeginFrame和_handleDrawFrame两个方法。
3.2 _handleBeginFrame
[源码路径:flutter/lib/src/scheduler/binding.dart]
void _handleBeginFrame(Duration rawTimeStamp) {
if (_warmUpFrame) { // 如果已经是在warmUpFrame过程中,则直接返回;因为warmUpFrame做的事情是一样的
assert(!_ignoreNextEngineDrawFrame);
_ignoreNextEngineDrawFrame = true;
return;
}
handleBeginFrame(rawTimeStamp); // 看下面
}
[源码路径:flutter/lib/src/scheduler/binding.dart]
void handleBeginFrame(Duration rawTimeStamp) {
Timeline.startSync('Frame', arguments: timelineWhitelistArguments);
_firstRawTimeStampInEpoch ??= rawTimeStamp; // 记录一些帧绘制相关的时间
_currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
if (rawTimeStamp != null)
_lastRawTimeStamp = rawTimeStamp;
assert(schedulerPhase == SchedulerPhase.idle); // 当前阶段是SchedulerPhase.idle
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map callbacks = _transientCallbacks;
_transientCallbacks = {};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack); // 回调_transientCallbacks
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
_handleBeginFrame主要做了两件事情:记录开始绘制相关的时间戳和遍历_transientCallbacks,逐一回调;
3.3 _handleDrawFrame
[源码路径:flutter/lib/src/scheduler/binding.dart]
void _handleDrawFrame() {
if (_ignoreNextEngineDrawFrame) {
_ignoreNextEngineDrawFrame = false;
return;
}
handleDrawFrame(); // 看下面
}
[源码路径:flutter/lib/src/scheduler/binding.dart]
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
Timeline.finishSync(); // end the "Animate" phase
try {
// 遍历_persistentCallbacks并进行回调
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// 遍历_postFrameCallbacks并进行回调
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List localPostFrameCallbacks =
List.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
Timeline.finishSync(); // end the Frame
_currentFrameTimeStamp = null;
}
}
_handleDrawFrame主要就是遍历_persistentCallbacks和_postFrameCallbacks,然后对里面注册的方法进行回调;
关于_persistentCallbacks和_postFrameCallbacks大家应该都不陌生,我们平时调用的WidgetsBinding.instance.addPersistentFrameCallback和WidgetsBinding.instance.addPostFrameCallback就是注册这里的回调。
恰巧的是,Flutter也注册了这些回调。
3.4 RendererBinding.initInstances
[源码路径:flutter/lib/src/rendering/binding.dart]
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
@override
void initInstances() {
super.initInstances();
_instance = this;
_pipelineOwner = PipelineOwner(
onNeedVisualUpdate: ensureVisualUpdate,
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
initRenderView();
_handleSemanticsEnabledChanged();
addPersistentFrameCallback(_handlePersistentFrameCallback); // 这里注册了回调
_mouseTracker = _createMouseTracker();
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame(); // 见3.4.1
}
因此,整个流程下来,当接收到Vsync时,会调用到RendererBinding的drawFrame方法;
3.4.1 drawFrame
[源码路径:flutter/lib/src/rendering/binding.dart]
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 触发RenderObject执行布局,见3.4.2
pipelineOwner.flushCompositingBits(); //绘制之前的预处理操作,见3.4.3
pipelineOwner.flushPaint(); // 触发RenderObject执行绘制,见3.4.4
renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU,见3.4.5
pipelineOwner.flushSemantics(); // 发送语义化给系统,用于辅助功能等
}
终于看到我们熟悉的pipelineOwner和renderView。关于这两个实例的构造过程请看上一篇文章。
3.4.2 flushLayout
[源码路径:flutter/lib/src/rendering/object.dart]
void flushLayout() {
...
try {
// TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves
while (_nodesNeedingLayout.isNotEmpty) {
final List dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = [];
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize(); // 见3.4.2.1
}
}
} finally {
...
}
}
这个方法主要做的事情就是遍历_nodesNeedingLayout,调用其_layoutWithoutResize方法。那么_nodesNeedingLayout保存的是什么呢?谁触发存呢?
_nodesNeedingLayout存的都是RenderObject,主要有两个途径保存:
- 一个是首次启动Flutter时,会把根结点RenderView存进去,具体请看上一篇文章3.3.5小节;
- 另一个是我们的Widget如果有导致布局需要更新的情况,会触发markNeedsLayout,那么Widget对应的RenderObject就会存到_nodesNeedingLayout里面。常见的栗子,例如setState之后,Container的child结点变成了另一个Widget;
所以,现在很清晰了,只要你的Widget改动需要重新layout的,那么Widget对应的RenderObject就会调用_layoutWithoutResize方法。
3.4.2.1 _layoutWithoutResize
[源码路径:flutter/lib/src/rendering/object.dart]
void _layoutWithoutResize() {
...
try {
performLayout(); // 见3.4.2.2
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
...
_needsLayout = false;
markNeedsPaint();
}
3.4.2.2 performLayout
[源码路径:flutter/lib/src/rendering/object.dart]
/// Do the work of computing the layout for this render object.
///
/// Do not call this function directly: call [layout] instead. This function
/// is called by [layout] when there is actually work to be done by this
/// render object during layout. The layout constraints provided by your
/// parent are available via the [constraints] getter.
///
/// If [sizedByParent] is true, then this function should not actually change
/// the dimensions of this render object. Instead, that work should be done by
/// [performResize]. If [sizedByParent] is false, then this function should
/// both change the dimensions of this render object and instruct its children
/// to layout.
///
/// In implementing this function, you must call [layout] on each of your
/// children, passing true for parentUsesSize if your layout information is
/// dependent on your child's layout information. Passing true for
/// parentUsesSize ensures that this render object will undergo layout if the
/// child undergoes layout. Otherwise, the child can change its layout
/// information without informing this render object.
@protected
void performLayout();
可以看到performLayout是一个接口方法,每个实现RenderObject都需要实现这个方法。
一般情况下,performLayout需要两件事情:
- 如果有child结点,需要对所有child结点调用它的layout方法;
- 把自身的大小保存到size变量里;
- 如果有需要,需要把child结点的偏移量offset保存到parentData,这样child结点才能知道自己从哪里开始绘制;
简单来说测量和布局都需要在这里完成。
可以看看一些常用的Widget对应的RenderObject它的performLayout方法,例如Stack(RenderStack)、Row(RenderFlex)、Image(RenderImage)等。
3.4.2.3 RenderView.performLayout
[源码路径:flutter/lib/src/rendering/view.dart]
@override
void performLayout() {
assert(_rootTransform != null);
_size = configuration.size; // 注释①
assert(_size.isFinite);
if (child != null)
child.layout(BoxConstraints.tight(_size)); // 注释②
}
RenderView作为Render树的根结点,毫无疑问Flutter App首次启动最先调用的肯定是RenderView.performLayout。
注释①
这里的configuration.size就是手机屏幕大小,所以这行的意思就是RenderView测量的大小就是屏幕大小。(这里不做跟踪分析,有需要自行跟代码看看)
注释②
RenderView的child,根据上一篇文章分析,它就是我们写的Widget对应的RenderObject。那么这里就是调用RenderObject.layout。
3.4.2.4 layout
[源码路径:flutter/lib/src/rendering/object.dart]
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;
} // 上面这段是为了确定relayoutBoundary
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_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();
}
layout方法是布局阶段最重要的方法。它是一个通用处理逻辑,每一个RenderObject不管是否自定义都是这个处理。
可以看到layout方法需要传入两个参数,第一个为constraints,即 父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。另外一个参数是 parentUsesSize,该值用于确定 relayoutBoundary,该参数表示子节点布局变化是否影响父节点,如果为true,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为false,则子节点布局发生变化后不会影响父节点。
(摘抄于https://book.flutterchina.club/chapter14/render_object.html)
第一个参数根结点RenderView传的是BoxConstraints.tight(_size)。
我们再看看layout方法里面都做了些啥?
注释①
relayoutBoundary是个很牛逼的东西。从它名字就知道它的作用就是圈出需要重新布局的范围。它是怎么用的呢?看这个方法就知道了:
[源码路径:flutter/lib/src/rendering/object.dart]
void markNeedsLayout() {
if (_needsLayout) {
return;
}
if (_relayoutBoundary != this) {
markParentNeedsLayout(); // 会调用parent.markNeedsLayout
} else {
_needsLayout = true;
if (owner != null) {
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
这个方法上面有提过,最常见调用它的路径是,setState的时候把某个Widget的child改成别的Widget。也就是只要某个Widget的child结点改变或者其他操作导致重新布局时,就会触发调用markNeedsLayout。
这个方法的处理逻辑就是:
- 先判断relayoutBoundary是不是自己,如果是就把自己存到_nodesNeedingLayout,等待下一次Vsync对自己重新布局layout;
- 如果relayoutBoundary不是自己,那就向上找问parent要relayoutBoundary。所以这是个递归,直到找到relayoutBoundary为止。
那么这段代码作用就很明确了,那就是如果这个Widget有变动,它需要找到被它影响到的范围,而这个范围就是relayoutBoundary。然后需要对relayoutBoundary重新进行layout。
所以当我们写自定义RenderObject时,实现performLayout的时候,要想清楚parentUsesSize参数要怎么传,因为它会影响relayoutBoundary,也就是会影响重新layout的范围。原则当然是范围尽可能小,提高渲染效率。
注释②
sizedByParent 意为该节点的大小是否仅通过 parent 传给它的 constraints 就可以确定了,即该节点的大小与它自身的属性和其子节点无关,比如如果一个控件永远充满 parent 的大小,那么 sizedByParent就应该返回true,此时其大小在 performResize() 中就确定了,在后面的 performLayout() 方法中将不会再被修改了,这种情况下 performLayout() 只负责布局子节点。(摘抄于https://book.flutterchina.club/chapter14/render_object.html)
sizedByParent默认为false,一般是在集成RenderObject重写。当我们自定义RenderObject可以考虑是否需要把该值设为true;
注释③
performLayout前面一路分析下来。这里performLayout调用的就是我们写Widget对应RenderObject的performLayout。如果这个Widget有child结点,就会调用child.layout,如果没有child结点,那就会对自身做测量和布局。如此形式一个递归循环,遍历整一棵Render树。
至此,flushLayout分析完成。
3.4.3 flushCompositingBits
[源码路径:flutter/lib/src/rendering/object.dart]
void flushCompositingBits() {
if (!kReleaseMode) {
Timeline.startSync('Compositing bits');
}
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();
if (!kReleaseMode) {
Timeline.finishSync();
}
}
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 (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
这个方法主要是做一些绘制前的预处理,检查RenderObject是否需要重绘。如果需要就调用markNeedsPaint方法。
3.4.4 flushPaint
[源码路径:flutter/lib/src/rendering/object.dart]
void flushPaint() {
if (!kReleaseMode) {
Timeline.startSync('Paint', arguments: timelineWhitelistArguments);
}
try {
final List dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = [];
// 按深度排序脏结点,最深度排第一位
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); // 见3.4.4.1
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {
if (!kReleaseMode) {
Timeline.finishSync();
}
}
}
3.4.4.1 repaintCompositedChild
[源码路径:flutter/lib/src/rendering/object.dart]
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
if (child._layer == null) {
child._layer = OffsetLayer();
} else {
child._layer.removeAllChildren();
}
childContext ??= PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero); // 见3.4.4.2
childContext.stopRecordingIfNeeded();
}
3.4.4.2 _paintWithContext
[源码路径:flutter/lib/src/rendering/object.dart]
void _paintWithContext(PaintingContext context, Offset offset) {
// If we still need layout, then that means that we were skipped in the
// layout phase and therefore don't need painting. We might not know that
// yet (that is, our layer might not have been detached yet), because the
// same node that skipped us in layout is above us in the tree (obviously)
// and therefore may not have had a chance to paint yet (since the tree
// paints in reverse order). In particular this will happen if they have
// a different layer, because there's a repaint boundary between us.
if (_needsLayout)
return;
RenderObject debugLastActivePaint;
_needsPaint = false;
try {
paint(context, offset); // 核心方法
} catch (e, stack) {
_debugReportException('paint', e, stack);
}
}
void paint(PaintingContext context, Offset offset) { }
_paintWithContext会调用RenderObject的paint方法。
paint方法默认是空实现,如果需要绘制内容,需要重写这个方法。
它跟performLayout方法类似,重写paint的时候,如果有child结点并且想绘制它的内容,则需要调用PaintingContext.paintChild去绘制child;否则,通过PaintingContext.canvas调用Canvas的Api去绘制自身内容;
可以看看各个RenderObject的paint实现源码,例如RenderStack、RenderImage等;
3.4.4.3 RenderView.paint
[源码路径:flutter/lib/src/rendering/view.dart]
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}
按照惯例,我们先看看根结点RenderView的paint实现。非常简单粗暴,就是只渲染child结点。这里的child就是我们写的Widget对应的RenderObject。
3.4.4.4 PaintingContext.paintChild
[源码路径:flutter/lib/src/rendering/object.dart]
/// Paint a child [RenderObject].
///
/// If the child has its own composited layer, the child will be composited
/// into the layer subtree associated with this painting context. Otherwise,
/// the child will be painted into the current PictureLayer for this context.
void paintChild(RenderObject child, Offset offset) {
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset); // 递归调用child结点的_paintWithContext方法
}
}
这里又会调用会child结点的_paintWithContext方法,回到3.4.4.2。
3.4.5 RenderView.compositeFrame
[源码路径:flutter/lib/src/rendering/view.dart]
/// Uploads the composited layer tree to the engine.
///
/// Actually causes the output of the rendering pipeline to appear on screen.
void compositeFrame() {
Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);
scene.dispose();
} finally {
Timeline.finishSync();
}
}
这个方法中有一个Scene对象,Scene对象是一个数据结构,保存最终渲染后的像素信息。这个方法将Canvas画好的Scene传给window.render()方法,该方法会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上。(摘抄于https://book.flutterchina.club/chapter14/flutter_app_startup.html)
3.4.6 小结
上面整个流程下来就是每一次Vsync信号,在Dart层所做的处理。核心就是执行drawFrame方法。而drawFrame方法主要有三个步骤:
- 遍历需要layout的RenderObject,让它执行performLayout方法;整个调用栈是:RenderView.performLayout->child.layout->child.performLayout->child.layout->...;
- 遍历需要paint的RenderObject,让它执行paint方法;整个调用栈是:RenderView.paint->PaintingContext.paintChild->child.paint->PaintingContext.paintChild->...;
- 通过PaintingContext.canvas可以把RenderObject绘制的内容绘制到PaintingContext._currentLayer上,最终构造出Scene实例,通过Window.render方法把Scene发送给Engine层,最终由Engine将内容渲染在设备屏幕上。
4、回顾问题
系列文章开始时提出了几个问题,经过系列文章分析后,基本上所有问题都已经有答案了:
我写的Widget是怎么渲染出来呢?(上一篇有解答)
为什么有时候Container是撑满父亲,有时候又不是?
答:是否撑满父亲应该是在layout过程去处理,那只需要看Container对应的RenderObject如何layout就可以找到答案。(如果看了Container的build源码就大概能理解)我写的Widget明明这么复杂,为啥可以被频繁build重新创建,性能还这么好?(上一篇有解答)
我们都知道有Widget树、Element树、RenderObject树,但是为什么要设计这么多层?Element树究竟有啥用?(上一篇有解答)
那个渲染溢出(overflow)的错误提示好烦啊,它是怎么出来的?
答:错误提示也是渲染出来,所以是在paint那里处理的。那么只要看看Row对应的RenderObject也就是RenderFlex的paint实现就能明白。
5、参考资料
布局类组件简介
RenderObject和RenderBox
Flutter渲染机制—UI线程
深入了解Flutter界面开发