Vsync 机制
在分析首帧渲染的过程中,可以发现Render Tree
的渲染逻辑(handleDrawFrame
方法)是直接执行的,但是后续每一帧的渲染都是Framework的主动调用导致的吗?实际上并非如此,也不能如此。试想一下,如果由Framework层控制每一帧的渲染,那么可能某一帧还没渲染完成,屏幕就开始刷新了,因为屏幕是按照自己的固有频率刷新的,而不会考虑具体的软件逻辑。此时,可能用于渲染的Buffer
中,一半是当前帧的数据,一半是上一帧的数据,这就是所谓的“撕裂”(Tearing),如图5-9所示。
为了避免撕裂,大部分UI框架都会引入Vsync机制,Vsync是垂直同步(Vertical Synchronization)的简称,其基本的思路是同步帧的渲染和显示器的刷新率。下面开始分析Flutter的Vsync机制。
Vsync 准备阶段
当UI需要更新一帧(通常是由于动画、手势或者直接调用setState
导致Element Tree
中出现脏节点)时,会调用ensureVisualUpdate
方法, 如果没有处于渲染状态,将调用scheduleFrame
方法。ensureVisualUpdate
代码如下:
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
scheduleFrame
代码如下:
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) return;
ensureFrameCallbacksRegistered();
window.scheduleFrame();
_hasScheduledFrame = true;
}
其中ensureFrameCallbacksRegistered
方法代码如下:
// 代码清单5-25 flutter/packages/flutter/lib/src/scheduler/binding.dart
void ensureFrameCallbacksRegistered() {
window.onBeginFrame ??= _handleBeginFrame; // 由代码清单5-35调用
window.onDrawFrame ??= _handleDrawFrame; // 由代码清单5-35调用
}
void _handleBeginFrame(Duration rawTimeStamp) {
if (_warmUpFrame) { // 首帧仍在渲染,见代码清单5-21
_rescheduleAfterWarmUpFrame = true;
return;
}
handleBeginFrame(rawTimeStamp); // 见代码清单5-36
}
void _handleDrawFrame() {
if (_rescheduleAfterWarmUpFrame) { // 首帧预渲染导致的调用
_rescheduleAfterWarmUpFrame = false;
addPostFrameCallback((Duration timeStamp) {
_hasScheduledFrame = false;
scheduleFrame(); // 见代码清单5-20
});
return;
}
handleDrawFrame(); // 见代码清单5-37
}
window.onBeginFrame
和window.onDrawFrame
将在注册的Vsync信号到达后调用,这部分内容后面将详细分析,其对应的接口分别调用了handleBeginFrame
方法和handleDrawFrame
方法,后面将详细分析其逻辑。
需要注意的是,如果_warmUpFrame
字段为true
,即通过Vsync
驱动的渲染开始时发现首帧渲染仍在进行,则将_rescheduleAfterWarmUpFrame
标记为true
,并在_handleDrawFrame
中注册一个回调后退出,该回调将在首帧渲染后请求再次渲染一帧, 而这一帧将是通过Vsync
信号驱动的。
下面分析window.scheduleFrame
接口的逻辑,其对应的是一个Engine方法,如代码清单5-26所示。
// 代码清单5-26 engine/lib/ui/window/platform_configuration.cc
void ScheduleFrame(Dart_NativeArguments args) {
UIDartState::ThrowIfUIOperationsProhibited(); // 确保在UI线程中
UIDartState::Current()->platform_configuration()->client()->ScheduleFrame(); // 见代码清单5-27
} // RuntimeController是client的具体实现类
以上逻辑主要检查当前是否处于UI线程,然后调用RuntimeController
的ScheduleFrame
方法,最终会调用Animator
的RequestFrame
方法,如代码清单5-27所示。
// 代码清单5-27 engine/shell/common/animator.cc
void Animator::RequestFrame(bool regenerate_layer_tree) {
if (regenerate_layer_tree) { // 仅有Platform View更新,复用上一帧的Layer Tree
regenerate_layer_tree_ = true; // 如果Animator已停止,则返回,但是如果屏幕配置发生改变(即第2个字段为true)
}
if (paused_ && !dimension_change_pending_) { return; } // 仍会请求Vsync信号
if (!pending_frame_semaphore_.TryWait()) { return; } // 已经有正在渲染的帧,返回
// 见代码清单5-34
task_runners_.GetUITaskRunner()->PostTask([ ...... ]() {
if (!self) { return; }
self->AwaitVSync(); // 见代码清单5-28
});
frame_scheduled_ = true; // 注意,比上一句逻辑先执行
}
以上逻辑中,regenerate_layer_tree
用于表示是否重新生成Flutter的渲染数据,一般情况下为true
,仅当UI中存在Platform View
且只有该部分需要更新时才为false
。接着检查当前是否可以注册Vsync,并通过AwaitVSync
发起注册。由于是PostTask
方式,因此frame_scheduled
会在此之前标记为true
,表示当前正在计划渲染一帧。AwaitVSync
方法的逻辑如代码清单5-28所示。
// 代码清单5-28 engine/shell/common/animator.cc
void Animator::AwaitVSync() {
waiter_->AsyncWaitForVsync( // Vsync信号到达后将触发的逻辑
[self = weak_factory_.GetWeakPtr()](fml::TimePoint vsync_start_time, // Vsync信号到达的时间
fml::TimePoint frame_target_time) { // 根据帧率计算的一帧绘制完成的最晚的时间点
if (self) { // 见代码清单5-33
if (self->CanReuseLastLayerTree()) { // 可以复用上一帧的Layer Tree
self->DrawLastLayerTree();
} else { // 开始渲染新的一帧
self->BeginFrame(vsync_start_time, frame_target_time);
}
}
}); // 通知Dart VM:当前处于等待Vsync的空闲状态,非常适合进行GC等行为
delegate_.OnAnimatorNotifyIdle(dart_frame_deadline_);
}
以上逻辑中,OnAnimatorNotifyIdle
方法将通知 Dart VM 当前处于空闲状态,用于驱动 GC(Garbage Collection,垃圾回收)等逻辑的执行,因为当前将要注册Vsync并等待其信号,所以肯定不会更新UI,非常适合进行GC等行为。AsyncWaitForVsync
方法负责Vsync监听的注册,下面进行分析。
Vsync 注册阶段
代码清单5-28中,AsyncWaitForVsync
方法将继续Vsync
信号的注册,其逻辑如代码清单5-29所示。
// 代码清单5-29 engine/shell/common/vsync_waiter.cc
void VsyncWaiter::AsyncWaitForVsync(const Callback& callback) {
if (!callback) {
return; // 若没有设置回调,则监听没有意义,直接返回
}
TRACE_EVENT0("flutter", "AsyncWaitForVsync");
{
std::scoped_lock lock(callback_mutex_);
if (callback_) { return; } // 说明有其他逻辑注册过,直接返回
callback_ = std::move(callback); // 赋值
if (secondary_callback_) { return; } // 说明有其他逻辑注册过,无须再次注册,返回
}
AwaitVSync(); // 具体的实现由平台决定,Android平台的实现见代码清单5-30
}
以上逻辑中,callback
是必须携带的参数,因为没有回调的注册没有意义。接着,会检查callback_
字段是否已经被设置,如果有则说明其他逻辑已经注册过了,直接返回;如果为null
,则赋值为当前参数。注意,这里会接着检查secondary_callback_
是否有值,其一般由触摸事件触发,其赋值时会触发AwaitVSync
,所以此时callback_
只需要完成赋值即可,而无须重复注册Vsync
信号。以上两个回调会在后面Vsync
信号到达时一并处理。
完成以上逻辑后,将调用AwaitVSync
方法开始正式注册Vsync
信号,如代码清单5-30所示。
// 代码清单5-30 engine/shell/platform/android/vsync_waiter_android.cc
void VsyncWaiterAndroid::AwaitVSync() {
auto* weak_this = new std::weak_ptr<VsyncWaiter>(shared_from_this());
jlong java_baton = reinterpret_cast<jlong>(weak_this); // 将当前实例变成long类型的id
task_runners_.GetPlatformTaskRunner()->PostTask([java_baton]() { // 切换线程
JNIEnv* env = fml::jni::AttachCurrentThread(); // 确保JNIEnv已准备完毕
env->CallStaticVoidMethod(g_vsync_waiter_class->obj(), // Embedder中的FlutterJNI实例
g_async_wait_for_vsync_method_, // 对应的Embedder方法
java_baton); // Vsync到达后通过该参数调用本对象
});
}
以上逻辑将在Java侧调用(在Platform
线程中),这是因为NDK中没有监听硬件信号Vsync
的API,而Android SDK中有。注意,java_baton
是当前对象的弱引用指针,将用于Vsync
信号到达时触发对应的回调。
该逻辑将调用Java侧的FlutterJNI
对象的asyncWaitForVsync
方法,最终将调用AsyncWaitForVsyncDelegate
的asyncWaitForVsync
方法。由代码清单4-3可知,该对象在启动时已经完成注册,具体逻辑如代码清单5-31所示。
// 代码清单5-31 engine/shell/platform/android/io/flutter/view/VsyncWaiter.java
private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate =
new FlutterJNI.AsyncWaitForVsyncDelegate() {
public void asyncWaitForVsync(long cookie) { // cookie即代码清单5-30中的java_baton
Choreographer.getInstance().postFrameCallback( // 为下一个Vsync信号注册回调
new Choreographer.FrameCallback() {
public void doFrame(long frameTimeNanos) { // Vsync到达时触发
float fps = windowManager.getDefaultDisplay().getRefreshRate(); // 设备帧率
long refreshPeriodNanos = (long) (1000000000.0 / fps); // 渲染一帧的最大耗时
FlutterJNI.nativeOnVsync( // 调用Engine的方法,见代码清单5-32
frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
}
);
}
};
以上逻辑主要是调用系统API,该API将在下一个Vsync
信号到达时调用doFrame
方法,其中,由frameTimeNanos
表示的Vsync
信号到达的时间会稍稍早于该回调发生的时间,因为从Vsync
信号到达到doFrame
被调用,中间必然有一些逻辑耗时。接着通过系统API获取当前设备的帧率,并计算绘制一帧所需要的时间,对fps
为60
的设备而言,绘制一帧需要16.6ms
。最后将通过FlutterJNI
调用Native
方法,开始进行Vsync
的响应,其中,第1个参数是Vsync
信号到达时间;第2个参数表示当前帧最晚完成绘制的时间,它将贯穿整个渲染流程;第3个参数表示Flutter Engine中响应该信号的对象的指针。
Vsync 响应阶段
下面开始分析nativeOnVsync
方法对应的Engine中的逻辑,如代码清单5-32所示。
// 代码清单5-32 engine/shell/platform/android/vsync_waiter_android.cc
void VsyncWaiterAndroid::OnNativeVsync( ...... ) {
TRACE_EVENT0("flutter", "VSYNC");
auto frame_time = fml::TimePoint::FromEpochDelta( // 时间格式转换
fml::TimeDelta::FromNanoseconds(frameTimeNanos));
auto target_time = fml::TimePoint::FromEpochDelta(
fml::TimeDelta::FromNanoseconds(frameTargetTimeNanos));
ConsumePendingCallback(java_baton, frame_time, target_time);
}
void VsyncWaiterAndroid::ConsumePendingCallback( ...... ) {
auto* weak_this = reinterpret_cast<std::weak_ptr<VsyncWaiter>*>(java_baton);
auto shared_this = weak_this->lock(); // 获取代码清单5-30中发起监听的VsyncWaiter实例
delete weak_this;
if (shared_this) { // 触发回调,以上由具体平台实现,以下是VsyncWaiter通用逻辑
shared_this->FireCallback(frame_start_time, frame_target_time);
}
}
以上逻辑首先提取frame_time
和target_time
,其含义前面内容已解释过。其次调用ConsumePendingCallback
,将java_baton
还原成进行注册的实例,并调用其Callback
,具体逻辑如代码清单5-33所示。
// 代码清单5-33 engine/shell/common/vsync_waiter.cc
void VsyncWaiter::FireCallback( ...... ) {
Callback callback;
fml::closure secondary_callback;
{
std::scoped_lock lock(callback_mutex_);
callback = std::move(callback_); // callback_的赋值逻辑位于代码清单5-29
secondary_callback = std::move(secondary_callback_);
}
if (!callback && !secondary_callback) { return; } // 没有回调,返回
if (callback) {
auto flow_identifier = fml::tracing::TraceNonce();
task_runners_.GetUITaskRunner()->PostTaskForTime([ ...... ]() {
callback(frame_start_time, frame_target_time); // 触发:见代码清单5-28
}, frame_start_time);
}
if (secondary_callback) {
task_runners_.GetUITaskRunner()->PostTaskForTime(
std::move(secondary_callback), frame_start_time);
}
}
以上逻辑提取callback_
和secondary_callback_
,如果都为null
则说明没有任何响应逻辑;如果有回调则在UI线程依次调用。对callback_
字段而言,其赋值在代码清单5-28中,其BeginFrame
方法如代码清单5-34所示。
// 代码清单5-34 engine/shell/common/animator.cc
void Animator::BeginFrame( ...... ) {
// SKIP 第1步,Trace & Timeline存储
frame_scheduled_ = false; // 当前不处于等待Vsync信号的状态
notify_idle_task_id_++; // idle(空闲状态)的计数id,每帧递增,作用见后面内容
regenerate_layer_tree_ = false; // 默认不重新生产layer_tree
pending_frame_semaphore_.Signal(); // 释放信号,允许接收新的Vsync信号请求,见代码清单5-27
if (!producer_continuation_) { // 第2步,产生一个待渲染帧,详见5.2.5节
producer_continuation_ = layer_tree_pipeline_->Produce(); // 见代码清单5-40
if (!producer_continuation_) { // 当前待渲染帧的队列已满
RequestFrame(); // 重新注册Vsync,在下一帧尝试加入队列,见代码清单5-27
return;
}
} // 第3步,重要属性的存储
last_frame_begin_time_ = fml::TimePoint::Now(); // 帧渲染实际开始时间
last_vsync_start_time_ = vsync_start_time; // Vsync信号通知的开始时间
last_frame_target_time_ = frame_target_time; // 当前帧完成渲染的最晚时间
dart_frame_deadline_ = FxlToDartOrEarlier(frame_target_time); // Dart VM的当前时间戳
{ // 第4步,开始渲染
delegate_.OnAnimatorBeginFrame(frame_target_time); // 见代码清单5-35
}
if (!frame_scheduled_) { // 第5步,在UI线程注册任务,用于通知Dart VM当前空闲,可以进行GC等操作
task_runners_.GetUITaskRunner()->PostDelayedTask([ ...... ]() {
if (!self) { return; }
if (notify_idle_task_id == self->notify_idle_task_id_ && // 没有正在渲染的帧
!self->frame_scheduled_) { // 不等待Vsync信号(准备渲染)
self->delegate_.OnAnimatorNotifyIdle(Dart_TimelineGetMicros() + 100000);
} // 以上判断的核心是保证当前确实处于空闲状态
}, kNotifyIdleTaskWaitTime);
}
}
以上逻辑相对来说比较复杂,主要分为5步。第1步和第3步主要是一些重要属性的存储,代码中已有说明。第2步涉及一个复杂的设计,将在后面单独分析。第4步将触发Framework的渲染逻辑,这部分内容会在后面详细分析。第5步将在当前帧的渲染完成之后,在UI线程注册一个任务,同样是用于通知Dart VM当前处于空闲状态,可以进行GC等操作。这是非常有必要的,因为一帧绘制完成后Framework层会有大量对象的创建与销毁。该任务发出后能够执行的条件是当前没有新的帧待渲染(即保证渲染的优先级始终高于GC),具体表现为代码中的两个条件。
-
notify_idle_task_id == self->notify_idle_task_id_
:说明当前没有正在渲染的帧,否则后者会自增。 -
!self->frame_scheduled_
:说明没有正在等待Vsync信号的帧,否则该属性为true
。
下面开始分析渲染的逻辑。OnAnimatorBeginFrame
方法将经由Shell、Engine、Runtime-Controller
最终调用PlatformConfiguration
的BeginFrame
方法,如代码清单5-35所示。
// 代码清单5-35 engine/lib/ui/window/platform_configuration.cc
void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime) {
// 完成渲染的最晚时间
std::shared_ptr<tonic::DartState> dart_state = begin_frame_.dart_state().lock();
if (!dart_state) { return; }
tonic::DartState::Scope scope(dart_state);
int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds();
tonic::LogIfError( tonic::DartInvoke(begin_frame_.Get(), // 见代码清单5-25
{ Dart_NewInteger(microseconds),})
);
UIDartState::Current()->FlushMicrotasksNow(); // 处理微任务
tonic::LogIfError(tonic::DartInvokeVoid(draw_frame_.Get())); // 见代码清单5-25
}
以上逻辑将调用Framework的window.onBeginFrame
和window.onDrawFrame
方法,并在中间调用FlushMicrotasksNow
方法以处理Dart VM的微任务。由此可以推断,帧渲染的逻辑将主要由Framework执行,下面开始详细分析。
Framework 响应阶段
由代码清单5-25可知,window.onBeginFrame
和window.onDrawFrame
接口所绑定的Dart 函数分别是handleBeginFrame
方法和handleDrawFrame
方法。前者逻辑如代码清单5-36所示。
// 代码清单5-36 flutter/packages/flutter/lib/src/scheduler/binding.dart
void handleBeginFrame(Duration? rawTimeStamp) {
Timeline.startSync('Frame', arguments: ......);
// 与时间戳相关字段的更新
try {
Timeline.startSync('Animate', arguments: ......); // Timeline事件
_schedulerPhase = SchedulerPhase.transientCallbacks; // 更新状态
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
// 见代码清单8-36
_transientCallbacks = <int, _FrameCallbackEntry>{}; // 处理高优先级的一次性回调
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!,
callbackEntry.debugStack);
});
_removedIds.clear();
} finally { // 更新状态,代码清单5-35中触发微任务消费
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
以上逻辑主要处理_transientCallbacks
字段中注册的回调,一般由动画注册,所以Timeline的第一个参数为’Animate
’,然后将_schedulerPhase
字段标记为midFrameMicrotasks
。由代码清单5-35可知,handleBeginFrame
方法执行完后确实会先处理完微任务(Micro Task),再触发handleDrawFrame
方法的执行,如代码清单5-37所示。
// 代码清单5-37 flutter/packages/flutter/lib/src/scheduler/binding.dart
void handleDrawFrame() {
Timeline.finishSync(); // 结束 Animate 阶段的统计
try {
_schedulerPhase = SchedulerPhase.persistentCallbacks; // 开始处理永久性回调
for (final FrameCallback callback in _persistentCallbacks) // 一般是3棵树的更新
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
_schedulerPhase = SchedulerPhase.postFrameCallbacks; // 处理低优先级的一次性回调
final List<FrameCallback> localPostFrameCallbacks = // 当前帧渲染完成后触发
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
} finally {
_schedulerPhase = SchedulerPhase.idle; // Framework的帧渲染工作完成,当前进入空闲状态
Timeline.finishSync(); // 结束帧,需要注意的是Raster线程仍将继续帧渲染工作
_currentFrameTimeStamp = null;
}
}
以上逻辑将依次处理_persistentCallbacks
字段和_postFrameCallbacks
字段中注册的回调,前者在启动过程中由Framework注册,执行后不会清除;后者一般由用户注册,每帧执行完之后都会清除。drawFrame
方法为_persistentCallbacks
字段中的主要逻辑,由于继承关系,将首先执行WidgetsBinding
的逻辑,如代码清单5-38所示。
// 代码清单5-38 flutter/packages/flutter/lib/src/widgets/binding.dart
// WidgetsBinding
void drawFrame() {
// SKIP 首帧耗时统计相关
try { // 开始更新3棵树,
if (renderViewElement != null) // Element Tree的根节点
buildOwner!.buildScope(renderViewElement!); // 见代码清单5-46
super.drawFrame(); // 见代码清单5-39
buildOwner!.finalizeTree(); // 见代码清单5-52
} finally { ...... }
// SKIP 首帧耗时统计相关
}
以上逻辑将执行buildScope
方法,其主要工作是更新Element Tree的脏节点,并同步更新Render Tree。super.drawFrame
方法则会根据Render Tree的信息完成Layout、Paint等工作,具体逻辑如代码清单5-39所示。finalizeTree
则会在UI线程的帧渲染工作结束后执行,主要负责清理Element Tree的无用节点,相关逻辑将在后面详细分析。
// 代码清单5-39 flutter/packages/flutter/lib/src/rendering/binding.dart
// RendererBinding
void drawFrame() {
pipelineOwner.flushLayout(); // 见代码清单5-55
pipelineOwner.flushCompositingBits(); // 见代码清单5-63
pipelineOwner.flushPaint(); // 见代码清单5-67
if (sendFramesToEngine) {
renderView.compositeFrame(); // 见代码清单5-83
pipelineOwner.flushSemantics();
_firstFrameSent = true;
}
}
以上逻辑中,flushLayout
方法负责更新Render Tree中每个节点的大小(Size)和位置(Offset)信息;flushPaint
方法负责遍历Render Tree,执行每个节点的Paint
逻辑,并生成Layer Tree。compositeFrame
方法负责从Layer Tree构建Scene
对象,并将通过Engine完成帧数据的最终渲染逻辑。
以上就是Flutter的Vsync机制:通过Framework发出请求,Engine将请求注册到Embedder的API中,并在Vsync信号到达时通过Engine回调到Framework,期间将先从UI线程切换到Platform线程,再从Platform线程切换回UI线程。
Continuation 设计分析
在代码清单5-34中,有一个非常晦涩的逻辑没有分析,即producer_continuation_
,因为它并不是能够简单地通过该方法的上下文可以理解的。从Vsync信号到达之后,一帧的数据经过Build、Layout、Paint等各个阶段,到真正开始渲染时,该对象都会一直存在,如果分散解读,很有可能因为忽略了这个对象而无法窥见渲染管道的全貌,因此本节将单独分析。
代码清单5-34中的layer_tree_pipeline_
对象在Animator
的构造函数中完成初始化,并在开始渲染前调用Produce
方法,其逻辑如代码清单5-40所示。
// 代码清单5-40 engine/shell/common/pipeline.h
explicit Pipeline(uint32_t depth) // 该参数默认为2
: depth_(depth), empty_(depth), available_(0), inflight_(0) {}
ProducerContinuation Produce() {
if (!empty_.TryWait()) { // 尝试产生一个待渲染帧,非阻塞式等待
return {}; // empty_ 初始值为2,每次生产一帧计数减1,故最多能生产2帧
}
++inflight_; // 待渲染帧数量加1
return ProducerContinuation{ // 当前待渲染帧尚无数据,故在此绑定提交数据的函数
std::bind(&Pipeline::ProducerCommit, // 见代码清单5-41
this, std::placeholders::_1, std::placeholders::_2), // 参数占位符
GetNextPipelineTraceID()};
}
由于渲染管道涉及多个线程,因此通过信号量empty_
控制待渲染帧的数量,以上命名中的Continuation
表示当前渲染一帧的任务存在但尚未完成。因为Vsync信号到达之后就已经确定要渲染一帧了,所以这里立刻通过Produce
过程完成这个标记,这样的好处是将Vsync信号和最终的帧渲染一一对应。由于信号量的存在,将不会存在一个Vsync信号导致多帧渲染的情况,因为生产者和消费者是一一对应的。
在完成Framework层的渲染逻辑后,一帧的数据至此才算完全准备好,此时可以告知代码清单5-34中产生的producer_continuation_
对象了,准确来说是提交待渲染的数据。具体的提交逻辑是调用前面内容绑定的ProducerCommit
函数,如代码清单5-41所示。
// 代码清单5-41 engine/shell/common/pipeline.h
bool ProducerCommit(ResourcePtr resource, size_t trace_id) { // 见代码清单5-97
{
std::scoped_lock lock(queue_mutex_);
queue_.emplace_back(std::move(resource), trace_id); // 将当前数据加入待渲染队列
}
available_.Signal(); // 计数1,新增一帧可用于渲染的资源
return true;
}
注意,以上逻辑只是将待渲染数据(resource
)提交到layer_tree_pipeline_
对象的待渲染帧队列中,这里为什么不直接渲染呢?一是当前还是UI线程,无法渲染;二是渲染前的准备工作尚未完成,因此通过队列暂存。这里available_
字段的作用和empty_
字段相似,都是控制待渲染帧的数量。
在渲染相关的工作完全准备好之后,Rasterizer
的Draw
方法将对队列中的待渲染帧的数据进行消费,如代码清单5-42所示。
// 代码清单5-42 engine/shell/common/pipeline.h
[[nodiscard]] PipelineConsumeResult Consume(const Consumer& consumer) {
if (consumer == nullptr) { return PipelineConsumeResult::NoneAvailable; } // 没有消费者
if (!available_.TryWait()) { // 没有可消费的帧数据
return PipelineConsumeResult::NoneAvailable;
}
ResourcePtr resource;
size_t trace_id = 0;
size_t items_count = 0;
{ // 取出第1个资源,进行处理
std::scoped_lock lock(queue_mutex_);
std::tie(resource, trace_id) = std::move(queue_.front()); // 提取数据
queue_.pop_front(); // 移除队列的第1个待渲染帧数据
items_count = queue_.size();
}
{
TRACE_EVENT0("flutter", "PipelineConsume");
consumer(std::move(resource)); // 一般将执行Rasterizer的DoDraw方法,见代码清单5-99
}
empty_.Signal(); // 释放资源,计数加1,可以响应新的渲染请求,见代码清单5-40
--inflight_; // 标记当前待渲染的帧数量减1
return items_count > 0 ? PipelineConsumeResult::MoreAvailable // 仍有待渲染的帧
: PipelineConsumeResult::Done; // 剩余0帧待渲染,完成
}
由于渲染是在Raster
线程中进行,因此这里的锁是十分有必要的,以上逻辑的核心是取出layer_tree_pipeline_
对象的待渲染帧队列中的第一个数据,并通过consumer
函数进行真正的消费。
总的来说,Continuation
的存在解决了以下两个问题。
-
Vsync信号与待渲染帧一一对应的问题(信号量)。
-
UI线程数据生产与Raster线程数据消费的时序问题(锁)。
此外,Continuation
还通过trace_id
为每一帧的渲染提供了追踪能力。虽然producer_continuation_
的调用点十分分散,但是从设计上来说,Continuation
保证了功能的解耦。如图5-10所示,Vsync信号到达后,Animator
产生一个Continuation
实例,Framework和Engine在UI线程完成一帧数据的合成并通过Continuation
对象提交给layer_tree_pipeline_
字段,Rasterizer
在真正渲染时再进行读取,流程和层次都十分清晰。
图5-10中,pipeline
所扮演的角色其实就是Android中的BufferQueue
,即连接渲染数据的生产者(Framework)和消费者(Rasterizer)。
Flutter 绘制原理
Flutter中和绘制相关的对象有三个,分别是Canvas
、Layer
和 Scene
:
Canvas
:封装了 Flutter Skia 各种绘制指令,比如画线、画圆、画矩形等指令。Layer
:分为容器类和绘制类两种;暂时可以理解为是绘制产物的载体,比如调用Canvas
的绘制 API 后,相应的绘制产物被保存在PictureLayer.picture
对象中。Scene
:屏幕上将要要显示的元素。在上屏前,我们需要将Layer中保存的绘制产物关联到Scene
上。
Flutter 绘制流程:
-
构建一个
Canvas
,用于绘制;同时还需要创建一个绘制指令记录器,因为绘制指令最终是要传递给Skia
的,而Canvas
可能会连续发起多条绘制指令,指令记录器用于收集Canvas
在一段时间内所有的绘制指令,因此Canvas
构造函数第一个参数必须传递一个PictureRecorder
实例。 -
Canvas
绘制完成后,通过PictureRecorder
获取绘制产物,然后将其保存在Layer
中。 -
构建
Scene
对象,将layer
的绘制产物和Scene
关联起来。 -
上屏,调用
window.render
API 将Scene
上的绘制产物发送给GPU。
下面我们通过一个实例来演示整个绘制流程:
还记得之前绘制棋盘的例子吗,之前无论是通过CustomPaint
还是自定义RenderObject
,都是在Flutter的Widget框架模型下进行的绘制,实际上,最终到底层Flutter都会按照上述的流程去完成绘制,既然如此,那么我们也可以直接在main
函数中调用这些底层API来完成,下面我们演示一下直接在main
函数中在屏幕中绘制棋盘。
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
,关于 Picture
有两点需要阐明:
Picture
实际上是一系列的图形绘制操作指令,这一点可以参考 Picture
类源码的注释。
Picture
要显示在屏幕上,必然会经过光栅化,随后Flutter会将光栅化后的位图信息缓存起来,也就是说同一个 Picture
对象,其绘制指令只会执行一次,执行完成后绘制的位图就会被缓存起来。
综合以上两点,我们可以看到 PictureLayer
的“绘制产物”一开始是一些列“绘图指令”,当第一次绘制完成后,位图信息就会被缓存,绘制指令也就不会再被执行了,所以这时“绘制产物”就是位图了。为了便于理解,后续我们可以认为指的就是绘制好的位图。
Canvas绘制的位图转图片
既然 Picture
中保存的是绘制产物,那么它也应该能提供一个方法能将绘制产物导出,实际上,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
作为绘制产物的持有者有什么作用? 答案就是:
- 可以在不同的
frame
之间复用绘制产物(如果没有发生变化)。 - 划分绘制边界,缩小重绘范围。
Layer
关键类及其继承关系如下:
在图5-14中,Layer
是Layer Tree
中所有节点的基类,其子类主要分为3种。
- 第1种是
ContainerLayer
,顾名思义就是其他Layer
节点的容器,比如OpacityLayer
为子节点增加一个透明度的效果,ClipRectLayer
对子节点进行裁剪。我们将直接继承自ContainerLayer
类的 Layer 称为容器类Layer,容器类 Layer 可以添加任意多个子Layer。 - 第2种是
PictureLayer
,保存绘制产物的 Layer,该Layer
是负责执行实际绘制的节点,该节点通过_picture
字段持有一个ui.PictureRecorder
对象,用于Engine进行对应绘制指令的记录。我们将可以直接承载(或关联)绘制结果的 Layer 称为绘制类 Layer。 - 第3种是
TextureLayer
和PlatformViewLayer
,它们的渲染源将由外部提供,并通过Layer
纳入Flutter的帧渲染中。
除此之外,PaintingContext
是Layer
进行绘制的上下文,提供进行最终绘制的Canvas
对象。ui.EngineLayer
则是Flutter Framework中的Layer
在Engine中的表示,其结构和Framework中的Layer
结构(即图5-14)几乎一致,在此不再赘述。
容器类 Layer 的作用
上面介绍的容器类 Layer 的概念,那么它的作用和具体使用场景是什么呢?
-
将组件树的绘制结构组成一棵树。
因为 Flutter 中的 Widget 是树状结构,那么相应的
RenderObject
对应的绘制结构也应该是树状结构,Flutter 会根据一些“特定的规则” 为组件树生成一棵 Layer 树,而容器类Layer就可以组成树状结构(父 Layer 可以包含任意多个子 Layer,子Layer又可以包含任意多个子Layer)。 -
可以对多个 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 实现变换效果的原理。容器类 Layer的变换在底层是通过 Skia
来实现的,不需要 Canvas
来处理。具体的原理是,有变换功能的容器类 Layer 会对应一个 Skia
引擎中的 Layer
,为了和 Flutter framework 中 Layer 区分,flutter 中将 Skia
的 Layer
称为 engine layer
。而有变换功能的容器类 Layer 在添加到 Scene
之前就会构建一个 engine layer
,我们以 OffsetLayer
为例,看看其相关实现:
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
主要是提供一种在绘制过程中缓存中间绘制结果的手段,为了在绘制复杂对象时方便多个绘制元素之间分离绘制而设计的,更多关于Canvas layer
相关API可以查阅相关文档,我们可以简单认为不管Canvas
对创建多少个layer
,这些layer
都是在同一个PictureLayer
上。
示例
下面我们看一段代码,了解它所对应的 Layer
是什么样子的:
void main() {
var imageUrl = " ...... ";
var direct = TextDirection.ltr;
runApp(Container(
child: Row(textDirection: direct,
children: [
RepaintBoundary(
child: Image.network(imageUrl, width: 100, excludeFromSemantics: true,)),
Opacity(opacity: 0.5,
child: Image.network(imageUrl, width: 100, excludeFromSemantics: true,))
],),));
Timer.run(() { // 输出 Widget Tree/Render Tree/Layer Tree的信息
debugDumpApp();
debugDumpRenderTree();
debugDumpLayerTree();
});
}
以上代码所对应的 Render Tree 及 Layer Tree 如图所示:
所以 Flutter 中实际上存在着4棵树,即 Layer Tree,使用 Layer Tree 的好处是可以做Paint流程的局部更新(没错,Flutter中局部更新的思想无处不在),比如视频播放时其上面的“播放”按钮、进度条等控件没有必要每一帧都进行 Paint。此外,Flutter的列表正是借助 Layer Tree 实现高效滑动:Flutter 的列表中,每个 Item 拥有一个独立的 Layer,这样在滑动的时候只需要更新 Layer 的位置信息,而不需要重新绘制内容。
组件树绘制流程
绘制相关实现在渲染对象 RenderObject
中,RenderObject
中和绘制相关的主要属性有:
layer
isRepaintBoundary
(bool
类型)needsCompositing
(bool
类型)
绘制边界节点
我们将 isRepaintBoundary
属性值为 true
的 RenderObject
节点称为绘制边界节点。
Flutter 自带了一个 RepaintBoundary
组件,它的功能其实就是向组件树中插入一个绘制边界节点。
needsCompositing
Render Tree 中,每个RenderObject
对象都拥有一个needsCompositing
属性,用于判断自身及其子节点是否有一个要去合成的图层(若为true
则说明自身拥有一个独立的图层),同时还有一个_needsCompositingBitsUpdate
字段用于标记该属性是否需要更新。Flutter 在 Paint 开始前首先会完成needsCompositing
属性的更新,然后开始正式绘制。
我们先讲一下Flutter绘制组件树的一般流程,注意,并非完整流程,因为我们暂时会忽略子树中需要“层合成”(Compositing
)的情况,这部分我们会在后面讲到。下面是大致流程:
Flutter第一次绘制时,会从上到下开始递归的绘制子节点,每当遇到一个边界节点,则判断如果该边界节点的 layer
属性为空(类型为ContainerLayer
),就会创建一个新的 OffsetLayer
并赋值给它;如果不为空,则直接使用它。然后会将边界节点的 layer
传递给子节点,接下来有两种情况:
- 如果子节点是非边界节点,且需要绘制,则会在第一次绘制时:
1)创建一个Canvas
对象和一个PictureLayer
,然后将它们绑定,后续调用Canvas
绘制都会落到和其绑定的PictureLayer
上。
2)接着将这个PictureLayer
加入到边界节点的layer
中。 - 如果不是第一次绘制,则复用已有的
PictureLayer
和Canvas
对象 。 - 如果子节点是边界节点,则对子节点递归上述过程。当子树的递归完成后,就要将子节点的
layer
添加到父级Layer
中。
整个流程执行完后就生成了一棵 Layer Tree。下面我们通过一个例子来理解整个过程:下图左边是 widget 树,右边是最终生成的 Layer 树
我们看一下生成过程:
RenderView
是 Flutter 应用的根节点,绘制会从它开始,因为他是一个绘制边界节点,在第一次绘制时,会为他创建一个OffsetLayer
,我们记为OffsetLayer1
,接下来OffsetLayer1
会传递给Row
.- 由于
Row
是一个容器类组件且不需要绘制自身,那么接下来他会绘制自己的孩子,它有两个孩子,先绘制第一个孩子Column1
,将OffsetLayer1
传给Column1
,而Column1
也不需要绘制自身,那么它又会将OffsetLayer1
传递给第一个子节点Text1
。 Text1
需要绘制文本,他会使用OffsetLayer1
进行绘制,由于OffsetLayer1
是第一次绘制,所以会新建一个PictureLayer1
和一个Canvas1
,然后将Canvas1
和PictureLayer1
绑定,接下来文本内容通过Canvas1
对象绘制,Text1
绘制完成后,Column1
又会将OffsetLayer1
传给Text2
。Text2
也需要使用OffsetLayer1
绘制文本,但是此时OffsetLayer1
已经不是第一次绘制,所以会复用之前的Canvas1
和PictureLayer1
,调用Canvas1
来绘制文本。Column1
的子节点绘制完成后,PictureLayer1
上承载的是Text1
和Text2
的绘制产物。- 接下来
Row
完成了Column1
的绘制后,开始绘制第二个子节点RepaintBoundary
,Row
会将OffsetLayer1
传递给RepaintBoundary
,由于它是一个绘制边界节点,且是第一次绘制,则会为它创建一个OffsetLayer2
,接下来RepaintBoundary
会将OffsetLayer2
传递给Column2
,和Column1
不同的是,Column2
会使用OffsetLayer2
去绘制Text3
和Text4
,绘制过程同Column1,在此不再赘述。 - 当
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。
Mark 阶段 markNeedsRepaint
RenderObject
是通过调用 markNeedsRepaint
来发起重绘请求的,在介绍 markNeedsRepaint
具体做了什么之前,我们根据上面介绍的 Flutter绘制流程先猜一下它应该做些什么?
我们知道绘制过程存在Layer共享,所以重绘时,需要重绘所有共享同一个Layer
的组件。比如上面的例子中,Text1
发生了变化,那么我们除了 Text1
也要重绘 Text2
;如果 Text3
发生了变化,那么也要重绘Text4
;那如何实现呢?
因为Text1
和 Text2
共享的是 OffsetLayer1
,而 OffsetLayer1
的拥有者是谁呢?找到它让它重绘不就行了!OK,可以很容发现 OffsetLayer1
的拥有者是根节点 RenderView
,它同时也是 Text1
和 Text2
的第一个父级绘制边界节点。同样的,OffsetLayer2
也正是 Text3
和 Text4
的第一个父级绘制边界节点,所以我们可以得出一个结论:当一个节点需要重绘时,我们得找到离它最近的第一个父级绘制边界节点,然后让它重绘即可,而 markNeedsRepaint
正是完成了这个过程,当一个节点调用了它时,具体的步骤如下:
- 会从当前节点一直往父级查找,直到找到一个绘制边界节点时终止查找,然后会将该绘制边界节点添加到其
PiplineOwner
的_nodesNeedingPaint
列表中(保存需要重绘的绘制边界节点)。 - 在查找的过程中,会将自己到绘制边界节点路径上所有节点的
_needsPaint
属性置为true
,表示需要重新绘制。 - 请求新的
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 { // 非RenderObject节点
// 一般不会发生,即当前节点不是一个`RenderObject`节点,此时直接请求帧渲染。
if (owner != null)
owner!.requestVisualUpdate();
}
}
值得一提的是,在当前版本的Flutter中是永远不会走到最后一个else
分支的,因为当前版本中根节点是一个RenderView
,而该组件的isRepaintBoundary
属性为 true
,所以如果调用 renderView.markNeedsPaint()
是会走到isRepaintBoundary
为 true
的分支的。
请求新的 frame 后,下一个 frame 到来时就会走drawFrame
流程,回忆一下该方法:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
//下面是 展开 super.drawFrame() 方法
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
...
}
}
drawFrame
中和绘制相关的涉及flushCompositingBits
、flushPaint
和 compositeFrame
三个函数,而重新绘制的流程在 flushPaint
中,所以我们先重点看一下flushPaint
的流程,关于 flushCompositingBits
,它涉及组件树中Layer
的合成,我们会在后面介绍 。
Flush 阶段 flushPaint
flushPaint
方法如下:
void flushPaint() {
if (!kReleaseMode) { Timeline.startSync('Paint', arguments: ......); }
// 开始绘制
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
for (final 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();
}
} // if
} // for
} finally {
if (!kReleaseMode) { Timeline.finishSync(); } // 结束绘制
}
}
以上逻辑会按照深度顺序,从最深的节点开始依次调用repaintCompositedChild
方法。需要注意的是,前几个阶段都是从深度最小的节点开始处理,但是Paint
阶段要从深度最大的节点开始,因为祖先的节点的Paint
效果必须作用于子节点,比如一个裁剪节点,要对子节点产生裁剪效果,必须等子节点完成绘制才行。PaintingContext
的repaintCompositedChild
方法最终会调用_repaintCompositedChild
方法。
这里需要提醒一点,我们在介绍stateState
流程一节说过,组件树中某个节点要更新自己时会调用markNeedsRepaint
方法,而该方法会从当前节点一直往上查找,直到找到一个isRepaintBoundary
为 true
的节点,然后会将该节点添加到 nodesNeedingPaint
列表中。因此,nodesNeedingPaint
中的节点的isRepaintBoundary
必然为 true
,换句话说,能被添加到 nodesNeedingPaint
列表中节点都是绘制边界,那么这个边界究竟是如何起作用的,我们继续看 PaintingContext._repaintCompositedChild
函数的实现。
// flutter/packages/flutter/lib/src/rendering/object.dart
static void _repaintCompositedChild( // PaintingContext
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
assert(child.isRepaintBoundary); // 断言:能走的这里,其isRepaintBoundary必定为true.
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) { //如果边界节点没有layer,则为其创建一个OffsetLayer
child._layer = childLayer = OffsetLayer();
} else { //如果边界节点已经有layer了(之前绘制时已经为其创建过layer了),则清空其子节点。
childLayer.removeAllChildren();
}
//通过其layer构建一个paintingContext,之后layer便和childContext绑定,这意味着通过同一个
//paintingContext的canvas绘制的产物属于同一个layer。
childContext ??= PaintingContext(child._layer!, child.paintBounds);
child._paintWithContext(childContext, Offset.zero); // 绘制子节点(树)
childContext.stopRecordingIfNeeded(); // 停止记录工作
}
可以看到,在绘制边界节点时会首先检查其是否有 layer
,如果没有就会创建一个新的 OffsetLayer
给它。
随后会根据该 offsetLayer
构建一个 PaintingContext
对象(记为childContext
),
其次,childContext
由参数传入,一般为null
,因此会新建一个PaintingContext
对象用于绘制该图层。之后子组件在获取context
的canvas
对象时会创建一个 PictureLayer
,然后再创建一个 Canvas
对象和新创建的 PictureLayer
关联起来,这意味着后续通过同一个childContext
的 canvas
绘制的产物属于同一个PictureLayer
。
_paintWithContext
方法主要负责当前子节点图层的绘制。
最后,在完成所有子节点的绘制后,调用stopRecordingIfNeeded
方法停止当前PaintingContext
对象(即childContext
)的记录工作。(Framework负责记录各种绘制指令,真正的绘制工作在Engine中进行 )
其中_paintWithContext
方法代码如下:
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout) return; // 异常情况:存在Layout未处理完的节点
_needsPaint = false;
try {
paint(context, offset); // 开始绘制
assert(!_needsLayout); // Layout阶段完成
assert(!_needsPaint); // Paint阶段完成
} catch (e, stack) { ...... }
}
void paint(PaintingContext context, Offset offset) { }
以上逻辑中,首先将_needsPaint
字段标记为false
,因为绘制即将开始,具体的绘制操作由RenderObject
子类所实现的paint
方法决定。该方法需要节点自己实现,用于绘制自身,节点类型不同,绘制算法一般也不同,不过功能是差不多的,即:如果是容器组件,要绘制孩子和自身(当然,容器自身也可能没有绘制逻辑,这种情况只绘制孩子即可,比如Center组件),如果不是容器类组件,则绘制自身(比如Image)。
以RenderView
为例,其最终将调用paintChild
方法,代码如下:
// flutter/packages/flutter/lib/src/rendering/object.dart
void paintChild(RenderObject child, Offset offset) {
if (child.isRepaintBoundary) { // 如果是绘制边界,则新建图层进行绘制
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else { // 否则直接基于当前图层和上下文进行绘制
child._paintWithContext(this, offset);
}
}
它的主要逻辑是:如果当前节点是边界节点,则停止当前图层的绘制,通过_compositeChild
新建一个图层开始当前节点的绘制,如果不是,则调用前面分析过的 _paintWithContext
方法基于当前图层开始执行Paint
逻辑。
其中,_compositeChild
方法如下:
// flutter/packages/flutter/lib/src/rendering/object.dart
void _compositeChild(RenderObject child, Offset offset) {
assert(child.isRepaintBoundary); // 目标节点是绘制边界,否则不会进入本逻辑
//如果子节点是边界节点,则递归调用repaintCompositedChild
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true); // 创建一个新的Layer
} else { ...... }
assert(child._layer is OffsetLayer);
//将孩子节点的layer添加到Layer树中,
final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
childOffsetLayer.offset = offset;
//将当前边界节点的layer添加到父边界节点的layer中.
appendLayer(child._layer!); // 加入Layer Tree
}
void appendLayer(Layer layer) { // 向Layer Tree中加入一个节点
assert(!_isRecording);
layer.remove();
_containerLayer.append(layer); // 后面分析
}
以上逻辑首先会调用前面分析的repaintCompositedChild
方法新建一个图层,并同步该图层的offset
信息,即该图层从哪里开始绘制。最后调用appendLayer
方法将新的图层加入当前的 Layer Tree。
这里需要注意三点:
- 绘制孩子节点时,如果遇到边界节点且当其不需要重绘(
_needsPaint
为false
) 时,会直接复用该边界节点的layer
,而无需重绘!这就是边界节点能跨 frame 复用的原理。 - 因为边界节点的
layer
类型是ContainerLayer
,所以是可以给它添加子节点。 - 注意是将当前边界节点的
layer
添加到 父边界节点,而不是父节点。
其中 _containerLayer
字段的append
方法如下:
// packages/flutter/lib/src/rendering/layer.dart
void append(Layer child) { // ContainerLayer
adoptChild(child);
child._previousSibling = lastChild; // 树结构的操作
if (lastChild != null) lastChild!._nextSibling = child;
_lastChild = child;
_firstChild ??= child;
}
void adoptChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) { // 如果总是需要合成,则不需要尝试标记
markNeedsAddToScene(); // 标记需要重新合成图层
}
super.adoptChild(child);
}
以上逻辑主要是完成子节点的挂载。在adoptChild
方法中,因为大部分Layer节点的alwaysNeedsAddToScene
属性均为false
,故都会调用markNeedsAddToScene
方法,表示当前节点需要加入Scene
的构建。Scene
是 Layer Tree 合成的最终产物。
按照上面的流程执行完毕后,最终所有边界节点的 layer
就会相连起来组成一棵 Layer Tree 。
RenderColoredBox 和 RenderOpacity 的 paint 方法分析
为了加深对Paint
的理解,下面分析两个典型的RenderObject
节点的paint
方法,首先是_RenderColoredBox
的paint
方法,如代码清单5-74所示。
// 代码清单5-74 flutter/packages/flutter/lib/src/widgets/basic.dart
// _RenderColoredBox
void paint(PaintingContext context, Offset offset) {
if (size > Size.zero) { // 绘制底色
context.canvas.drawRect(offset & size, Paint()..color = color); // 见代码清单5-80
} // 绘制子节点,子节点必须后绘制
if (child != null) { context.paintChild(child!, offset); }
}
以上逻辑首先会绘制目标区域的颜色,然后绘制子节点。而RenderOpacity
的paint
方法则要复杂一些,如代码清单5-75所示。
// 代码清单5-75 flutter/packages/flutter/lib/src/rendering/proxy_box.dart
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0) { // 全透明,相当于不存在
layer = null;
return; // 直接返回
}
if (_alpha == 255) { // 全不透明,即遮挡
layer = null;
context.paintChild(child!, offset); // 无需独立图层,直接绘制,相当于普通节点
return;
}
assert(needsCompositing); // 新增一个半透明的Layer节点,见代码清单5-76
layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as
OpacityLayer?);
}
}
RenderOpacity
只有在子节点存在时才会绘制,且全透明时直接返回,不透明时直接在当前PaintContext
对象中进行绘制,只有半透明时才会通过pushOpacity
方法在一个OpacityLayer
中进行绘制。
对于半透明的情况,最终将调用PaintingContext
的pushOpacity
方法,如代码清单5-76所示。
// 代码清单5-76 flutter/packages/flutter/lib/src/rendering/object.dart
OpacityLayer pushOpacity(Offset offset, int alpha,
PaintingContextCallback painter, { OpacityLayer? oldLayer }) {
final OpacityLayer layer = oldLayer ?? OpacityLayer();
layer // 对于透明度图层,只需要知道Alpha的值和绘制偏移即可
..alpha = alpha
..offset = offset;
pushLayer(layer, painter, Offset.zero); // 见代码清单5-77
return layer;
}
以上逻辑将使用当前节点(RenderOpacity
)的Layer
,如果没有则新建一个OpacityLayer
,并设置其透明度、偏移值等属性,然后将该Layer
加入Layer Tree,如代码清单5-77所示。
// 代码清单5-77 flutter/packages/flutter/lib/src/rendering/object.dart
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter,
Offset offset, { Rect? childPaintBounds }) {
assert(painter != null); // 第1步,移除当前Layer的所有子节点
if (childLayer.hasChildren) { childLayer.removeAllChildren(); } // 清空子节点
stopRecordingIfNeeded(); // 第2步,停止当前Layer的绘制,见代码清单5-78
appendLayer(childLayer); // 加入Layer Tree,见代码清单5-73
final PaintingContext childContext = // 第3步,创建新图层的PaintingContext
createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset); // 开始新图层的绘制,见代码清单5-79
childContext.stopRecordingIfNeeded(); // 新图层绘制完成,见代码清单5-78
}
PaintingContext createChildContext( ...... ) {
return PaintingContext(childLayer, bounds);
}
以上逻辑中,第1步,移除当前Layer的所有子节点。第2步,停止当前Layer的绘制,如代码清单5-78,然后调用appendLayer
方法将当前Layer加入Layer Tree,核心逻辑如代码清单5-73所示。第3步,创建一个新的PaintingContext
对象用于新图层的绘制,painter
由具体的RenderObject
节点传入,对于RenderOpacity
,其painter
方法如代码清单5-79所示。最后,结束当前图层的绘制并退出。
// 代码清单5-78 flutter/packages/flutter/lib/src/rendering/object.dart
// 重置相关变量
void stopRecordingIfNeeded() {
if (!_isRecording) return;
_currentLayer!.picture = _recorder!.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
以上逻辑用于结束一个Layer的绘制,主要是将相关字段设置为null
,endRecording
方法最终将调用Engine中的一个方法。
接下来分析OpacityLayer
真正的绘制逻辑,如代码清单5-79所示。
// 代码清单5-79 flutter/packages/flutter/lib/src/rendering/proxy_box.dart
mixin RenderProxyBoxMixin<T extends RenderBox>
on RenderBox, RenderObjectWithChildMixin<T> {
void paint(PaintingContext context, Offset offset) {
if (child != null) context.paintChild(child!, offset); // 见代码清单5-71
}
}
以上逻辑主要是对子节点进行绘制,paintChild
方法的逻辑在前面已介绍过。RenderOpacity
只负责提供带透明效果的Layer,而RenderParagraph
需要基于Canvas
进行绘制,其get
方法如代码清单5-80所示。
// 代码清单5-80 flutter/packages/flutter/lib/src/rendering/object.dart
// PaintingContext
Canvas get canvas { // 见代码清单5-74,绘制时使用
if (_canvas == null) _startRecording(); //如果canvas为空,则是第一次获取
return _canvas!;
}
// 创建PictureLayer和canvas
void _startRecording() {
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder(); // 记录所有的绘制指令
_canvas = Canvas(_recorder!); // 创建 canvas
//将pictureLayer添加到_containerLayer(是绘制边界节点的Layer)中
_containerLayer.append(_currentLayer!);
}
由以上逻辑可知,真正的绘制是在PictureLayer
中进行的,PictureRecorder
负责保存所有的绘制指令。
需要注意的是,以上逻辑中Canvas
、PictureRecorder
等都继承自NativeFieldWrapperClass2
类,该类是Dart提供的用于封装Native
(C++)对象的父类,故以上绘制操作(通过Canvas
,借由PictureRecorder
记录)都是Native
调用,这些调用将被合成为最终的上屏数据。
创建新的 PictureLayer
现在,我们在本节最开篇示例基础上,给 Row 添加第三个子节点 Text5
,如图,那么它的Layer 树会变成什么样的?
因为 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
上,所以最终的 Layer Tree 如图:
我们总结一下:父节点在绘制子节点时,如果子节点是绘制边界节点,则在绘制完子节点后会生成一个新的 PictureLayer,后续其他子节点会在新的 PictureLayer 上绘制。
原理我们搞清楚了,但是为什么要这么做呢?直接复用之前的 PictureLayer1
有问题吗?
- 答案是:在当前的示例中是不会有问题,但是在层叠布局(如
Stack
组件)的场景中就会有问题,下面我们看一个例子,结构图如下:
左边是一个 Stack
布局,右边是对应的 Layer Tree 结构;我们知道Stack
布局中会根据其子组件的加入顺序进行层叠绘制,最先加入的孩子在最底层,最后加入的孩子在最上层。可以设想一下如果绘制 Child3
时复用了 PictureLayer1
,则会导致 Child3
被 Child2
遮住,这显然不符合预期,但如果新建一个 PictureLayer
在添加到 OffsetLayer
最后面,则可以获得正确的结果。
现在我们再来深入思考一下:如果 Child2
的父节点不是 RepaintBoundary
,那么是否就意味着 Child3
和 Child1
就可以共享同一个 PictureLayer
了?
- 答案是否定的!如果
Child2
的父组件改为一个自定义的组件,在这个自定义的组件中我们希望对子节点在渲染时进行一些矩阵变化,为了实现这个功能,我们创建一个新的TransformLayer
并指定变换规则,然后我们把它传递给Child2
,Child2
会绘制完成后,我们需要将TransformLayer
添加到Layer
树中(不添加到Layer
树中是不会显示的),则组件树和最终的 Layer Tree 结构如图所示:
可以发现这种情况本质上和上面使用 RepaintBoudary
的情况是一样的,Child3
仍然不应该复用 PictureLayer1
,那么现在我们可以总结一个一般规律了:只要一个组件需要往 Layer 树中添加新的 Layer,那么就必须也要结束掉当前 PictureLayer 的绘制。 这也是为什么 PaintingContext
中需要往 Layer 树中添加新 Layer 的方法(比如pushLayer
、addLayer
)中都有如下两行代码:
stopRecordingIfNeeded(); //先结束当前 PictureLayer 的绘制
appendLayer(layer);// 再添加到 layer树
这是向 Layer 树中添加Layer
的标准操作。这个结论要牢记,我们在后面介绍 flushCompositingBits()
的原理时会用到。
综上,Layer树的最终结构大致如图所示(随便一个例子,并不和本例对应):
compositeFrame
创建好layer
后,接下来就需要上屏展示了,而这部分工作是由renderView.compositeFrame
方法来完成的。实际上他的实现逻辑很简单:先通过layer
构建Scene
,最后再通过window.render
API 来渲染:
void compositeFrame() {
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 Tree 中每一个layer
传给 Skia
(最终会调用native
API,如果想了解详情,建议查看 OffsetLayer
和 PictureLayer
的 addToScene
方法),这是上屏前的最后一个准备动作,最后就是调用 window.render
将绘制数据发给GPU渲染出来了!( window.render
会调用Native层Engine的Render
方法进行渲染工作,这里不做展开分析)
Layer 使用实例
本节通过优化之前“绘制棋盘示例“来像大家展示如何在自定义组件中使用Layer
。
通过 Layer 实现绘制缓存
我们之前绘制棋盘示例是使用的CustomPaint
组件,然后再painter
的paint
方法中同时实现了绘制棋盘和棋子,实际上这里可以有一个优化,因为棋盘是不会变化的,所以理想的方式就是当绘制区域不发生变化时,棋盘只需要绘制一次,当棋子发生变化时,每次只需要绘制棋子信息即可。
注意:在实际开发中,要实现上述功能还是优先使用Flutter建议的”Widget组合“的方式:比如棋盘和棋子分别绘制在两个
Widget
中,然后包上RepaintBoundary
组件后把他们添加到Stack
中,这样做到分层渲染。不过,本节主要是为了说明Flutter自定义组件中如何使用Layer
,所以我们采用自定义RenderObject
的方式来实现。
- 首先我们定义一个
ChessWidget
,因为它并非容器类组件,所以继承自LeafRenderObjectWidget
:
class ChessWidget extends LeafRenderObjectWidget {
RenderObject createRenderObject(BuildContext context) {
return RenderChess(); // 返回Render对象
}
//...省略updateRenderObject函数实现
}
由于自定义的 RenderChess
对象不接受任何参数,所以我们可以在ChessWidget
中不用实现updateRenderObject
方法。
- 实现
RenderChess
;我们先直接实现一个未缓存棋盘的原始版本,随后我们再一点点添加代码,直到把它改造成可以缓存棋盘的对象。
class RenderChess extends RenderBox {
void performLayout() {
//确定ChessWidget的大小
size = constraints.constrain(
constraints.isTight ? Size.infinite : Size(150, 150),
);
}
void paint(PaintingContext context, Offset offset) {
Rect rect = offset & size;
drawChessboard(canvas, rect); // 绘制棋盘
drawPieces(context.canvas, rect);//绘制棋子
}
}
- 接下来我们需要实现棋盘缓存,我们的思路是:
- 创建一个
Layer
专门绘制棋盘,然后缓存。 - 当重绘触发时,如果绘制区域发生了变化,则重新绘制棋盘并缓存;如果绘制区域未变,则直接使用之前的
Layer
为此,我们需要定义一个PictureLayer
来缓存棋盘,然后添加一个 _checkIfChessboardNeedsUpdate
函数来实现上述逻辑:
// 保存之前的棋盘大小
Rect _rect = Rect.zero;
PictureLayer _layer = PictureLayer()
_checkIfChessboardNeedsUpdate(Rect rect) {
// 如果绘制区域大小没发生变化,则无需重绘棋盘
if (_rect == rect) return;
// 绘制区域发生了变化,需要重新绘制并缓存棋盘
_rect = rect;
print("paint chessboard");
// 新建一个PictureLayer,用于缓存棋盘的绘制结果,并添加到layer中
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
drawChessboard(canvas, rect); //绘制棋盘
// 将绘制产物保存在pictureLayer中
_layer = PictureLayer(Rect.zero)..picture = recorder.endRecording();
}
void paint(PaintingContext context, Offset offset) {
Rect rect = offset & size;
//检查棋盘大小是否需要变化,如果变化,则需要重新绘制棋盘并缓存
_checkIfChessboardNeedsUpdate(rect);
//将缓存棋盘的layer添加到context中,每次重绘都要调用,原因下面会解释
context.addLayer(_layer);
//再画棋子
print("paint pieces");
drawPieces(context.canvas, rect);
}
具体的实现逻辑见注释,这里不再赘述,需要特别解释的是在 paint
方法中,每次重绘都需要调用 context.addLayer(_layer)
将棋盘layer
添加到当前的 Layer Tree 中,通过上一节的介绍,我们知道,实际上是添加到了当前节点的第一个绘制边界节点的Layer
中。可能会有人疑惑,如果棋盘不变的话,添加一次不就行了,为什么每次重绘都要添加?实际上这个问题我们上一节已经解释过了,因为重绘是当前节点的第一个父级向下发起的,而每次重绘前,该节点都会先清空所有的孩子,代码见 PaintingContext.repaintCompositedChild
方法,所以我们需要每次重绘时都要添加一下。
OK,现在我们已经实现了棋盘缓存了,下面我们来验证一下。
我们创建一个测试 Demo 来验证一下,我们创建一个 ChessWidget
和一个 ElevatedButton
,因为ElevatedButton
在点击时会执行水波动画,所以会发起一连串的重绘请求,而根据上一节的知识,我们知道ChessWidget
和 ElevatedButton
会在同一个Layer
上绘制,所以 ElevatedButton
重绘也会导致ChessWidget
的重绘。另外我们在绘制棋子和棋盘时都加了日志,所以我们只需要点击 ElevatedButton
,然后查看日志就能验证棋盘缓存是否生效。
注意:在当前版本(3.0)的Flutter中,
ElevatedButton
的实现中并没有添加RepaintBoundary
,所以它才会和ChessWidget
在同一个Layer
上渲染,如果后续 Flutter SDK中给ElevatedButton
添加了RepaintBoundary
,则不能通过本例来验证。
class PaintTest extends StatefulWidget {
const PaintTest({Key? key}) : super(key: key);
State<PaintTest> createState() => _PaintTestState();
}
class _PaintTestState extends State<PaintTest> {
ByteData? byteData;
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ChessWidget(),
ElevatedButton(
onPressed: () {
setState(() => null);
},
child: Text("setState"),
),
],
),
);
}
}
点击按钮后发现,棋盘、棋子都可以正常显示,如图:
同时日志面板输出了很多"paint pieces
",并没有"paint chessboard
",可见棋盘缓存生效了。
好的,貌似我们预期的功能已经实现了,但是别高兴太早,上面的代码还有一个内存泄露的坑,我们在下面 LayerHandle
部分介绍。
LayerHandle
上面 RenderChess
实现中,我们将棋盘绘制信息缓存到了 layer
中,因为 layer
中保存的绘制产物是需要调用 dispose
方法释放的,如果ChessWidget
销毁时没有释放则会发生内存泄露,所以们需要在组件销毁时,手动释放一下,给RenderChess
中添加如下代码:
void dispose() {
_layer.dispose();
super.dispose();
}
上面的场景比较简单,实际上,在Flutter中一个layer
可能会反复被添加到多个容器类Layer中,或从容器中移除,这样一来有些时候我们可能会搞不清楚一个layer
是否还被使用,为了解决这个问题,Flutter中定义了一个LayerHandle
类来专门管理layer
,内部是通过引用计数的方式来跟踪layer
是否还有使用者,一旦没有使用者,会自动调用layer.dispose
来释放资源。
为了符合Flutter规范,强烈建议在需要使用layer
的时候通过LayerHandle
来管理它。现在我们来修改一下上面的代码,RenderChess
中定义一个 layerHandle
,然后将_layer
全部替换为 layerHandle.layer
:
// 定义一个新的 layerHandle
final layerHandle = LayerHandle<PictureLayer>();
_checkIfChessboardNeedsUpdate(Rect rect) {
...
layerHandle.layer = PictureLayer(Rect.zero)..picture = recorder.endRecording();
}
void paint(PaintingContext context, Offset offset) {
...
//将缓存棋盘的layer添加到context中
context.addLayer(layerHandle.layer!);
...
}
void dispose() {
//layer通过引用计数的方式来跟踪自身是否还被layerHandle持有,
//如果不被持有则会释放资源,所以我们必须手动置空,该set操作会
//解除layerHandle对layer的持有。
layerHandle.layer = null;
super.dispose();
}
OK,这样就很好了!不过先别急着庆祝,现在我们再来回想一下上一节介绍的内容,每一个 RenderObject
都有一个layer
属性,我们能否直接使用它来保存棋盘layer
呢?下面我们看看 RenderObject
中关于 layer
的定义:
set layer(ContainerLayer? newLayer) {
_layerHandle.layer = newLayer;
}
final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();
可以发现,我们RenderObject
中已经定义了一个 _layerHandle
了,它会去管理 layer
;同时 layer
是一个setter
,会自动将新 layer
赋值到 _layerHandle
上,那么我们是否可以在 RenderChess
中直接使用父类定义好的 _layerHandle
,这样的话我们就无需再自定义一个 layerHandle
了。
答案是:取决于当前节点的 isRepaintBoundary
属性是否为 true
(即当前节点是否为绘制边界节点) ,如果为 true
则不可以,如果不为 true
,则可以。上一节中讲过,Flutter在执行 flushPaint
重绘时遇到绘制边界节点:
- 先检查其
layer
是否为空,如果不为空,则会先清空该layer
的孩子节点,然后会使用该layer
创建一个PaintingContext
,传递给paint
方法。 - 如果其
layer
为空,会创建一个OffsetLayer
给它。
如果我们要将棋盘layer
保存到预定义的 layer
变量中的话,得先创建一个ContainerLayer
,然后将绘制棋盘的PictureLayer
作为子节点添加到新创建的ContainerLayer
中,然后赋值给 layer
变量。这样一来:
- 如果我们设置
RenderChess
的isRepaintBoundary
为true
,那么在每次重绘时,flutter 框架都会将layer
子节点清空,这样的话,我们的棋盘Picturelayer
就会被移除,接下来就会触发异常。 - 如果
RenderChess
的isRepaintBoundary
为false
(默认值),则在重绘过程中 flutter 框架不会使用到layer
属性,这中情况没有问题。
虽然,本例中 RenderChess
的 isRepaintBoundary
为 false
,直接使用 layer是可以的,但我不建议这么做,原因有二:
RenderObject
中的layer
字段在 Flutter 框架中是专门为绘制流程而设计的,按照职责分离原则,我们也不应该去蹭它。即使现在能蹭成功,万一哪天Flutter的绘制流发生变化,比如也开始使用非绘制边界节点的layer
字段,那么我们的代码将会出问题。- 如果要使用
Layer
,我们也需要先创建一个ContainerLayer
,既然如此,我们还不如直接创建一个LayerHandle
,这更方便。
现在考虑最后一个问题,在上面示例中,我们点击按钮后,虽然棋盘不会重绘了,但棋子还是会重绘,这并不合理,我们希望棋盘区域不受外界干扰,只有新的落子行为时(点击在棋盘区域)时再重绘棋子。相信看到着,解决方案就呼之欲出了,我们有两种选择:
RenderChess
的isRepaintBoundary
返回true
;将当前节点变为一个绘制边界,这样ChessWidget
就会和按钮分别在不同的layer
上绘制,也就不会相互影响。- 在使用
ChessWidget
时,给它套一个RepaintBoundary
组件,和 1 的原理差不多的,只不过这种方式是将ChessWidget
的父节点(RepaintBoundary
)变为了绘制边界(而不是自身),这样也会创建一个新的layer
来隔离按钮的绘制。
具体应该选哪种应该根据情况而定,第二种方案会更灵活,但第一种方案的实际效果往往会比较好,因为如果我们封装的复杂自绘控件中没有设置 isRepaintBoundary
为 true
,我们很难保证使用者在使用时会给我们的控件添加RepaintBoundary
,所以这种细节还是对使用者屏蔽掉会比较好。
Compositing
下面我们来介绍一下 flushCompositingBits()
。现在,我们再来回顾一下Flutter的渲染管线:
void drawFrame(){
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame()
...//省略
}
其中只有 flushCompositingBits()
还没有介绍过,这是因为要理解flushCompositingBits()
,就必须的了解Layer是什么,以及 Layer 树构建的过程。为了更容易理解它,我们先看一个demo。
CustomRotatedBox
我们实现一个CustomRotatedBox
,它的功能是将其子元素放倒(顺时针旋转 90 度),要实现个效果我们可以直接使用 canvas
的变换功能,下面是核心代码:
class CustomRotatedBox extends SingleChildRenderObjectWidget {
CustomRotatedBox({Key? key, Widget? child}) : super(key: key, child: child);
RenderObject createRenderObject(BuildContext context) {
return CustomRenderRotatedBox();
}
}
class CustomRenderRotatedBox extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
void performLayout() {
_paintTransform = null;
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
//根据子组件大小计算出旋转矩阵
_paintTransform = Matrix4.identity()
..translate(size.width / 2.0, size.height / 2.0)
..rotateZ(math.pi / 2) // 旋转90度
..translate(-child!.size.width / 2.0, -child!.size.height / 2.0);
} else {
size = constraints.smallest;
}
}
void paint(PaintingContext context, Offset offset) {
if(child!=null){
// 根据偏移,需要调整一下旋转矩阵
final Matrix4 transform =
Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(_paintTransform!)
..translate(-offset.dx, -offset.dy);
_paint(context, offset, transform);
} else {
//...
}
}
void _paint(PaintingContext context,Offset offset,Matrix4 transform ){
// 为了不干扰其他和自己在同一个layer上绘制的节点,所以需要先调用save然后在子元素绘制完后
// 再调用restore显示,关于save/restore有兴趣可以查看Canvas API doc
context.canvas
..save()
..transform(transform.storage);
context.paintChild(child!, offset);
context.canvas.restore();
}
... //省略无关代码
}
下面我们写个demo测试一下:
class CustomRotatedBoxTest extends StatelessWidget {
const CustomRotatedBoxTest({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: Text(
"A",
textScaleFactor: 5,
),
),
);
}
}
运行效果如图,A 被成功放倒了:
现在我们给 CustomRotatedBox
添加一个 RepaintBoundary
再试试:
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: RepaintBoundary( // 添加一个 RepaintBoundary
child: Text(
"A",
textScaleFactor: 5,
),
),
),
);
}
运行后如图:
咦,A 怎么又站起来了!
我们来分析一下原因:根据上一节的知识,我们可以很容易画出添加 RepaintBoundary
前和后的 Layer Tree 结构,如图:
添加 RepaintBoundary
后,CustomRotatedBox
中的持有的还是 OffsetLayer1
:
void _paint(PaintingContext context,Offset offset,Matrix4 transform ){
context.canvas // 该 canvas 对应的是 PictureLayer1
..save()
..transform(transform.storage);
// 子节点是绘制边界节点,会在新的 OffsetLayer2中的 PictureLayer2 上绘制
context.paintChild(child!, offset);
context.canvas.restore();
}
... //省略无关代码
}
很显然,CustomRotatedBox
中进行旋转变换的 canvas
对应的是 PictureLayer1
,而 Text("A")
的绘制是使用的PictureLayer2
对应的 canvas
,他们属于不同的 Layer
。可以发现父子的 PictureLayer
“分离了”,所以CustomRotatedBox
也就不会对 Text("A")
起作用。那么如何解决这个问题呢?
我们在前面的小节介绍过,有很多容器类组件都附带变换效果,拥有旋转变换的容器类 Layer 是 TransformLayer
,那么我们就可以在 CustomRotatedBox
中绘制子节点之前:
- 创建一个
TransformLayer
(记为TransformLayer1
) 添加到 Layer树中,接着创建一个新的PaintingContext
和TransformLayer1
绑定。 - 子节点通过这个新的
PaintingContext
去绘制。
完成上述操作之后,后代节点绘制所在的 PictureLayer
都会是 TransformLayer
的子节点,因此我们可以通过 TransformLayer
对所有子节点整体做变换。下图是添加是 TransformLayer1
前、后的 Layer 树结构。
这其实就是一个重新 Layer 合成(layer compositing) 的过程:创建一个新的 ContainerLayer
,然后将该ContainerLayer
传递给子节点,这样后代节点的Layer
必然属于ContainerLayer
,那么给这个 ContainerLayer
做变换就会对其全部的子孙节点生效。
“Layer 合成” 在不同的语境会有不同的指代,比如 skia 最终渲染时也是将一个个 layer 渲染出来,这个过程也可以认为是多个 layer 上的绘制信息合成为最终的位图信息;另外 canvas 中也有 layer 的概念(
canvas.save
方法生成新的layer),对应的将所有layer 绘制结果最后叠加在一起的过程也可以成为 layer 合成。
下面我们看看具体代码实现。由于 Layer 的组合是一个标准的过程(唯一的不同是使用哪种ContainerLayer
来作为父容器),PantingContext
中提供了一个 pushLayer
方法来执行组合过程,我们看看其实现源码:
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
if (childLayer.hasChildren) {
childLayer.removeAllChildren();
}
//下面两行是向Layer树中添加新Layer的标准操作,在之前小节中详细介绍过,忘记的话可以去查阅。
stopRecordingIfNeeded();
appendLayer(childLayer);
//通过新layer创建一个新的childContext对象
final PaintingContext childContext =
createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
//painter是绘制子节点的回调,我们需要将新的childContext对象传给它
painter(childContext, offset);
//子节点绘制完成后获取绘制产物,将其保存到PictureLayer.picture中
childContext.stopRecordingIfNeeded();
}
那么,我们只需要创建一个 TransformLayer
然后指定我们需要的旋转变换,然后直接调用 pushLayer
可以:
// 创建一个持有 TransformLayer 的 handle.
final LayerHandle<TransformLayer> _transformLayer = LayerHandle<TransformLayer>();
void _paintWithNewLayer(PaintingContext context, Offset offset, Matrix4 transform) {
//创建一个 TransformLayer,保存在handle中
_transformLayer.layer = _transformLayer.layer ?? TransformLayer();
_transformLayer.layer!.transform = transform;
context.pushLayer(
_transformLayer.layer!,
_paintChild, // 子节点绘制回调;添加完layer后,子节点会在新的layer上绘制
offset,
childPaintBounds: MatrixUtils.inverseTransformRect(
transform,
offset & size,
),
);
}
// 子节点绘制回调
void _paintChild(PaintingContext context, Offset offset) {
context.paintChild(child!, offset);
}
然后我们需要在 paint
方法中判断一下子节点是否是绘制边界节点,如果是则需要走layer
组合,如果不是则需要走 layer
合成:
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Matrix4 transform =
Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(_paintTransform!)
..translate(-offset.dx, -offset.dy);
if (child!.isRepaintBoundary) { // 添加判断
_paintWithNewLayer(context, offset, transform);
} else {
_paint(context, offset, transform);
}
} else {
_transformLayer.layer = null;
}
}
为了让代码看起看更清晰,我们将child
不为空时的绘制逻辑逻辑封装一个 pushTransform
函数里:
TransformLayer? pushTransform(
PaintingContext context,
bool needsCompositing,
Offset offset,
Matrix4 transform,
PaintingContextCallback painter, {
TransformLayer? oldLayer,
}) {
final Matrix4 effectiveTransform =
Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)
..translate(-offset.dx, -offset.dy);
if (needsCompositing) {
final TransformLayer layer = oldLayer ?? TransformLayer();
layer.transform = effectiveTransform;
context.pushLayer(
layer,
painter,
offset,
childPaintBounds: MatrixUtils.inverseTransformRect(
effectiveTransform,
context.estimatedBounds,
),
);
return layer;
} else {
context.canvas
..save()
..transform(effectiveTransform.storage);
painter(context, offset);
context.canvas.restore();
return null;
}
}
然后修改一下 paint
实现,直接调用 pushTransform
方法即可:
void paint(PaintingContext context, Offset offset) {
if (child != null) {
pushTransform(
context,
child!.isRepaintBoundary,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
是不是清晰多了,现在我们重新运行一下示例,效果与前面一样,A被成功放倒了!
需要说明的是,其实 PaintingContext
已经帮我们封装好了 pushTransform
方法,我们可以直接使用它:
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.pushTransform(
child!.isRepaintBoundary,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
实际上,PaintingContext
针对常见的拥有变换功能的容器类Layer
的组合都封装好了相应的方法,同时Flutter中已经预定了拥有相应变换功能的组件,下面是一个对应表:
Layer的名称 | PaintingContext对应的方法 | Widget |
---|---|---|
ClipPathLayer | pushClipPath | ClipPath |
OpacityLayer | pushOpacity | Opacity |
ClipRRectLayer | pushClipRRect | ClipRRect |
ClipRectLayer | pushClipRect | ClipRect |
TransformLayer | pushTransform | RotatedBox、Transform |
什么时候需要合成 Layer ?
1. 合成 Layer 的原则
通过上面的例子我们知道 CustomRotatedBox
的直接子节点是绘制边界节点时 CustomRotatedBox
中就需要合成 layer
。实际上这只是一种特例,还有一些其他情况也需要 CustomRotatedBox
进行 Layer
合成,那什么时候需要 Layer
合成有没有一个一般性的普适原则?答案是:有! 我们思考一下 CustomRotatedBox
中需要 Layer
合成的根本原因是什么?如果 CustomRotatedBox
的所有后代节点都共享的是同一个PictureLayer
,但是,一旦有后代节点创建了新的PictureLayer
,则绘制就会脱离了之前PictureLayer
,因为不同的PictureLayer
上的绘制是相互隔离的,是不能相互影响,所以为了使变换对所有后代节点对应的 PictureLayer
都生效,则我们就需要将所有后代节点的添加到同一个 ContainerLayer
中,所以就需要在 CustomRotatedBox
中先进行 Layer
合成。
综上,一个普适的原则就呼之欲出了:当后代节点会向 layer 树中添加新的绘制类Layer时,则父级的变换类组件中就需要合成 Layer。
下面我们验证一下:
现在我们修改一下上面的示例,给 RepaintBoundary
添加一个 Center
父组件:
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: Center( // 新添加
child: RepaintBoundary(
child: Text(
"A",
textScaleFactor: 5,
),
),
),
),
);
}
因为 CustomRotatedBox
中只判断了其直接子节点的child!.isRepaintBoundary
为 true
时,才会进行 layer
合成,而现在它的直接子节点是Center
,所以该判断会是false
,则不会进行 layer
合成。但是根据我们上面得出的结论,RepaintBoundary
作为CustomRotatedBox
的后代节点且会向 layer 树中添加新 layer
时就需要进行 layer
合成,而本例中是应该合成layer
但实际上却没有合成,所以预期是不能将 “A” 放倒的,运行后发现效果是 ”A“ 果然并没有被放倒!
看来我们的 CustomRotatedBox
还是需要继续修改。解决这个问题并不难,我们在判断是否需要进行 Layer 合成时,要去遍历整个子树,看看否存在绘制边界节点,如果是则合成,反之则否。为此,我们新定义一个在子树上查找是否存在绘制边界节点的 needCompositing()
方法:
//子树中递归查找是否存在绘制边界
needCompositing() {
bool result = false;
_visit(RenderObject child) {
if (child.isRepaintBoundary) {
result = true;
return ;
} else {
//递归查找
child.visitChildren(_visit);
}
}
//遍历子节点
visitChildren(_visit);
return result;
}
然后需要修改一下 paint
实现:
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.pushTransform(
needCompositing(), //子树是否存在绘制边界节点
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
现在,我们再来运行一下demo,运行后效果:
又成功放倒了!但还有问题,我们继续往下看。
2. alwaysNeedsCompositing
我们考虑一下这种情况:如果 CustomRotatedBox
的后代节点中没有绘制边界节点,但是有后代节点向 layer 树中添加了新的 layer。这种情况下,按照我们之前得出的结论 CustomRotatedBox
中也是需要进行 layer 合成的,但 CustomRotatedBox 实际上并没有。问题知道了,但是这个问题却不好解决,原因是我们在 CustomRotatedBox
中遍历后代节点时,是无法知道非绘制边界节点是否往 layer 树中添加了新的 layer。怎么办呢?Flutter是通过约定来解决这个问题的:
-
RenderObject
中定义了一个布尔类型alwaysNeedsCompositing
属性。 -
约定:自定义组件中,如果组件
isRepaintBoundary
为false
时,在绘制时要会向 layer 树中添加新的 layer的话,要将alwaysNeedsCompositing
置为true
。
开发者在自定义组件时应该遵守这个规范。根据此规范,CustomRotatedBox
中我们在子树中递归查找时的判断条件就可以改为:
child.isRepaintBoundary || child.alwaysNeedsCompositing
最终 我们的needCompositing
实现如下:
//子树中递归查找是否存在绘制边界
needCompositing() {
bool result = false;
_visit(RenderObject child) {
// 修改判断条件改为
if (child.isRepaintBoundary || child.alwaysNeedsCompositing) {
result = true;
return ;
} else {
child.visitChildren(_visit);
}
}
visitChildren(_visit);
return result;
}
注意:这要求非绘制节点组件在向 layer 树中添加 layer 时必须的让自身的 alwaysNeedsCompositing
值为 ture
.
下面我们看一下 flutter 中 Opacity
组件的实现。
3. Opacity 解析
Opacity
可以对子树进行透明度控制,这个效果通过 canvas
是很难实现的,所以 flutter 中直接使用了 OffsetLayer
合成的方式来实现:
class RenderOpacity extends RenderProxyBox {
// 本组件是非绘制边界节点,但会在部分透明的情况下向layer树中添加新的Layer,所以部分透明时要返回 true
bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0) {
// 完全透明,则没必要再绘制子节点了
layer = null;
return;
}
if (_alpha == 255) {
// 完全不透明,则不需要变换处理,直接绘制子节点即可
layer = null;
context.paintChild(child!, offset);
return;
}
// 部分透明,需要通过OffsetLayer来处理,会向layer树中添加新 layer
layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
}
}
}
4. 优化
注意,上面我们通过 CustomRotatedBox
演示了变换类组件的核心原理,不过还有一些优化的地方,比如:
- 变换类组件中,遍历子树以确定是否需要 layer 合成是变换类组件的通用逻辑,不需要在每个组件里都实现一遍。
- 不是每一次重绘都需要去遍历子树,比如可以在初始化时遍历一次,然后将结果缓存,如果后续有变化,再重新遍历更新即可,此时直接使用缓存的结果。
Flutter 也考虑到了这个问题,于是便有了flushCompositingBits
方法,我们下面来正式介绍它。
flushCompositingBits
每一个节点(RenderObject
中)都有一个_needsCompositing
字段,该字段用于缓存当前节点在绘制子节点时是否需要合成 layer。flushCompositingBits
的功能就是在节点树初始化和子树中合成信息发生变化时来重新遍历节点树,更新每一个节点的_needsCompositing
值。可以发现:
递归遍历子树的逻辑抽到了 flushCompositingBits
中,不需要组件单独实现。
不需要每一次重绘都遍历子树了,只需要在初始化和发生变化时重新遍历。
完美的解决了我们之前提出的问题,下面我们看一下具体实现:
void flushCompositingBits() {
// 对需要更新合成信息的节点按照节点在节点树中的深度排序
_nodesNeedingCompositingBitsUpdate.sort((a,b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits(); //更新合成信息
}
_nodesNeedingCompositingBitsUpdate.clear();
}
RenderObject
的 _updateCompositingBits
方法的功能就是递归遍历子树确定如果每一个节点的_needsCompositing
值:
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
// 递归遍历查找子树, 如果有孩子节点 needsCompositing 为true,则更新 _needsCompositing 值
visitChildren((RenderObject child) {
child._updateCompositingBits(); //递归执行
if (child.needsCompositing)
_needsCompositing = true;
});
// 这行我们上面讲过
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
执行完毕后,每一个节点的_needsCompositing
就确定了,我们在绘制时只需要判断一下当前的 needsCompositing
(一个getter
,会直接返回_needsCompositing
) 就能知道子树是否存在剥离layer了。这样的话,我们可以再优化一下 CustomRenderRotatedBox
的实现,最终的实现如下:
class CustomRenderRotatedBox extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
Matrix4? _paintTransform;
void performLayout() {
_paintTransform = null;
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
//根据子组件大小计算出旋转矩阵
_paintTransform = Matrix4.identity()
..translate(size.width / 2.0, size.height / 2.0)
..rotateZ(math.pi / 2)
..translate(-child!.size.width / 2.0, -child!.size.height / 2.0);
} else {
size = constraints.smallest;
}
}
final LayerHandle<TransformLayer> _transformLayer =
LayerHandle<TransformLayer>();
void _paintChild(PaintingContext context, Offset offset) {
print("paint child");
context.paintChild(child!, offset);
}
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_transformLayer.layer = context.pushTransform(
needsCompositing, // pipelineOwner.flushCompositingBits(); 执行后这个值就能确定
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
void dispose() {
_transformLayer.layer = null;
super.dispose();
}
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (_paintTransform != null) transform.multiply(_paintTransform!);
super.applyPaintTransform(child, transform);
}
}
是不是简洁清晰了很多!
flushCompositingBits 存在的意义
现在,我们思考一下引入 flushCompositingBits
的根本原因是什么?假如我们在变换类容器中始终采用合成 layer 的方式来对子树应用变换效果,也就是说不再使用 canvas
进行变换,这样的话 flushCompositingBits
也就没必要存在了,为什么一定要 flushCompositingBits
呢?根本原因就是:如果在变换类组件中一刀切的使用合成 layer 方式的话,每遇到一个变换类组件则至少会再创建一个 layer,这样的话,最终 layer 树上的layer数量就会变多。我们之前说过对子树应用的变换效果既能通过 Canvas
实现也能通过容器类Layer实现时,建议使用Canvas
。这是因为每新建一个 layer 都会有额外的开销,所以我们只应该在无法通过 Canvas
来实现子树变化效果时再通过Layer 合成的方式来实现。综上,我们可以发现引入 flushCompositingBits
的根本原因其实是为了减少 layer的数量。
另外,flushCompositingBits
的执行过程只是做标记,并没有进行层的合成,真正的合成是在绘制时(组件的 paint
方法中)。
总结
-
只有组件树中有变换类容器时,才有可能需要重新合成
layer
;如果没有变换类组件,则不需要。 -
当变换类容器的后代节点会向
layer
树中添加新的绘制类layer
时,则变换类组件中就需要合成layer
。 -
引入
flushCompositingBits
的根本原因是为了减少layer
的数量。
Flutter 渲染管道流程总结
在图5-16中,Vsync
信号在到达Engine
后,首先完成动画的刷新,其次在Engine
中发起Dart VM
中微任务的处理,最后回到Framework
中,开始渲染管道的核心工作,主要包括Build、Layout、Paint、Composition、Rasterize
这5个阶段。
- 在
Build
阶段,将基于Widget Tree
,在Element Tree
(本质是BuildOwner
)的驱动下,完成Render Tree
原始数据的更新; - 在
Layout
阶段,Render Tree
将在PipelineOwner
的驱动下完成大小(Size
)和偏移(Offset
)等关键布局数据的计算; - 在
Paint
阶段,Render Tree
将基于PaintingContext
遍历每个节点,更新Framework
中的Layer Tree
; - 在
Composition
阶段,Engine
将以Framework
阶段生成的Layer Tree
为输入,合成最终的渲染数据Scene
,并提交到pipeline
; - 在
Rasterize
阶段,Rasterizer
将从pipeline
中取出待渲染数据,最终绘制在目标Surface
上,并显示给用户。
参考:
- 《Flutter实战·第二版》
- 《Flutter内核源码剖析》