Mécanisme Vsync
Lors du processus d'analyse du rendu de la première image, on peut constater Render Tree
que la logique de rendu ( handleDrawFrame
méthode) est exécutée directement, mais le rendu de chaque image suivante est-il causé par l'appel actif du Framework ? En réalité, il ne l'est pas et ne peut pas l'être. Imaginez, si le rendu de chaque image est contrôlé par la couche Framework, l'écran peut commencer à se rafraîchir avant qu'une certaine image ne soit rendue, car l'écran est rafraîchi selon sa propre fréquence naturelle, sans tenir compte de la logique logicielle spécifique. A ce moment, la moitié des données pouvant être utilisées pour le rendu Buffer
sont les données de l'image actuelle et l'autre moitié sont les données de l'image précédente.
Afin d'éviter les déchirures, la plupart des frameworks d'interface utilisateur introduiront le mécanisme Vsync . Vsync est l'abréviation de Synchronisation verticale (Vertical Synchronization). L'idée de base est de synchroniser le rendu des images et le taux de rafraîchissement de l'affichage. Commençons par analyser le mécanisme Vsync de Flutter.
Phase de préparation Vsync
La méthode sera appelée lorsque l'interface utilisateur doit mettre à jour une image (généralement en raison de nœuds sales dans l'animation, d'un geste ou d'un appel direct), setState
si elle n'est pas dans l'état de rendu, la méthode sera appelée. code afficher comme ci-dessous: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
code afficher comme ci-dessous:
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) return;
ensureFrameCallbacksRegistered();
window.scheduleFrame();
_hasScheduledFrame = true;
}
Le code de la méthode est le ensureFrameCallbacksRegistered
suivant :
// 代码清单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
et window.onDrawFrame
sera appelée après l'arrivée du signal Vsync enregistré . Cette partie sera analysée en détail plus tard. L'interface correspondante appelle handleBeginFrame
méthode et handleDrawFrame
méthode respectivement, et sa logique sera analysée en détail plus tard.
需要注意的是,如果_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
}
Dans la logique ci-dessus, callback
c'est un paramètre qui doit être transporté, car l'enregistrement sans rappel n'a pas de sens. Ensuite, il vérifiera callback_
si le champ a été défini, si c'est le cas, cela signifie qu'une autre logique a été enregistrée, et reviendra directement ; si c'est le cas, null
affectez le paramètre actuel. Notez qu'il vérifiera secondary_callback_
s'il y a une valeur, qui est généralement déclenchée par un événement tactile, et qu'elle sera déclenchée lorsqu'elle est attribuée AwaitVSync
, callback_
il suffit donc de terminer l'attribution à ce moment, sans répéter le Vsync
signal d'enregistrement. Les deux rappels ci-dessus seront Vsync
traités ensemble lorsque le signal suivant arrivera.
Une fois la logique ci-dessus terminée, AwaitVSync
la méthode sera appelée pour enregistrer officiellement Vsync
le signal, comme indiqué dans le Listing 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到达后通过该参数调用本对象
});
}
La logique ci-dessus sera appelée du côté Java (dans Platform
le thread), car il n'y a pas d'API pour écouter les signaux matériels dans le NDK , mais il y en a dans le SDK Android . Notez que c'est le pointeur de référence faible de l'objet courant qui sera utilisé pour déclencher le callback correspondant à l'arrivée du signal.Vsync
java_baton
Vsync
Cette logique appellera la méthode de l'objet côté Java , qui finira par appeler la méthode. Comme on peut le voir à partir de la liste de codes 4-3, l'objet a été enregistré au démarrage, et la logique spécifique est montrée dans la liste de codes 5-31.FlutterJNI
asyncWaitForVsync
AsyncWaitForVsyncDelegate
asyncWaitForVsync
// 代码清单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);
}
}
);
}
};
La logique ci-dessus consiste principalement à appeler l'API système, qui appellera la méthode lorsque le Vsync
signal suivant arrivera doFrame
, où l'heure d'arrivée du signal frameTimeNanos
indiquée par sera légèrement antérieure à l'heure à laquelle le rappel se produit, car à partir de l'arrivée du signal à l' appel, il doit y avoir un peu de logique qui prend du temps. Ensuite, obtenez la fréquence d'images de l'appareil actuel via l'API système et calculez le temps nécessaire pour dessiner une image . Enfin, la réponse sera lancée en appelant la méthode , où le premier paramètre est l'heure d'arrivée du signal ; le deuxième paramètre indique la dernière heure de dessin de l'image actuelle, qui se déroulera tout au long du processus de rendu ; le troisième paramètre indique le Flutter Moteur Pointeur vers l'objet qui a répondu au signal.Vsync
Vsync
doFrame
fps
60
16.6ms
FlutterJNI
Native
Vsync
Vsync
Phase de réponse Vsync
Commençons par analyser nativeOnVsync
la logique dans le moteur correspondant à la méthode, comme indiqué dans le Listing 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);
}
}
La logique ci-dessus extrait d'abord frame_time
la somme target_time
, dont la signification a été expliquée précédemment. Le deuxième appel ConsumePendingCallback
restaurera java_baton
l'instance enregistrée et l'appellera Callback
La logique spécifique est indiquée dans la liste de codes 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);
}
}
L'extraction logique ci-dessus callback_
et sum secondary_callback_
, s'ils sont tous les deux null
, cela signifie qu'il n'y a pas de logique de réponse ; s'il y a un rappel, il sera appelé séquentiellement sur le thread d'interface utilisateur. Pour callback_
les champs, l'affectation se trouve dans le Listing 5-28 et la BeginFrame
méthode est illustrée dans le Listing 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);
}
}
La logique ci-dessus est relativement complexe, principalement divisée en 5 étapes. Les étapes 1 et 3 sont principalement le stockage de certains attributs importants, qui ont été expliqués dans le code. L'étape 2 implique une conception complexe qui sera analysée séparément plus tard. L'étape 4 déclenchera la logique de rendu du Framework, qui sera analysée en détail ultérieurement. La cinquième étape consiste à enregistrer une tâche sur le fil d'interface utilisateur une fois le rendu de l'image actuelle terminé, qui est également utilisé pour informer la machine virtuelle Dart qu'elle est actuellement inactive et peut effectuer des opérations telles que GC. Ceci est très nécessaire, car le calque Framework créera et détruira un grand nombre d'objets après le dessin d'un cadre. La condition selon laquelle la tâche peut être exécutée après son émission est qu'il n'y a pas de nouvelle image à rendre (c'est-à-dire que la priorité du rendu est toujours supérieure à celle de GC), ce qui se manifeste spécifiquement dans les deux conditions du code .
-
notify_idle_task_id == self->notify_idle_task_id_
: Indique qu'il n'y a actuellement aucune image en cours de rendu, sinon celle-ci augmentera automatiquement. -
!self->frame_scheduled_
: Indique qu'il n'y a pas de trame en attente du signal Vsync, sinon cette propriété vauttrue
.
Commençons à analyser la logique de rendu. OnAnimatorBeginFrame
La méthode sera appelée via la méthode Shell、Engine、Runtime-Controller
finale , comme indiqué dans le Listing 5-35.PlatformConfiguration
BeginFrame
// 代码清单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
}
La logique ci-dessus appellera le Framework window.onBeginFrame
et window.onDrawFrame
la méthode, et appellera FlushMicrotasksNow
la méthode au milieu pour gérer la microtâche de la machine virtuelle Dart. On peut en déduire que la logique de rendu des images sera principalement exécutée par le Framework, et l'analyse détaillée commencera ci-dessous.
Phase de réponse du cadre
Comme on peut le voir dans la liste de codes 5-25, les fonctions Dart liées à l'interface sont respectivement window.onBeginFrame
des méthodes et des méthodes. La logique du premier est montrée dans le Listing 5-36.window.onDrawFrame
handleBeginFrame
handleDrawFrame
// 代码清单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;
}
}
La logique ci-dessus traite principalement _transientCallbacks
du rappel enregistré dans le champ, qui est généralement enregistré par animation, donc le premier paramètre de Timeline est ' Animate
', puis _schedulerPhase
le champ est marqué comme midFrameMicrotasks
. On peut voir à partir de la liste de codes 5-35 qu'après handleBeginFrame
l'exécution de la méthode, la micro-tâche (Micro Task) sera traitée en premier, puis handleDrawFrame
l'exécution de la méthode sera déclenchée, comme indiqué dans la liste de codes 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;
}
}
La logique ci-dessus traitera _persistentCallbacks
le champ et _postFrameCallbacks
le rappel enregistré dans le champ à tour de rôle. Le premier est enregistré par le Framework lors du processus de démarrage et ne sera pas effacé après l'exécution ; le second est généralement enregistré par l'utilisateur et sera effacé après chaque le cadre est exécuté. drawFrame
La méthode est _persistentCallbacks
la logique principale du champ. En raison de la relation d'héritage, WidgetsBinding
la logique sera exécutée en premier, comme indiqué dans le Listing 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 首帧耗时统计相关
}
La logique ci-dessus exécutera buildScope
la méthode, et sa tâche principale consiste à mettre à jour les nœuds sales de l'arbre d'éléments et à mettre à jour l'arbre de rendu de manière synchrone. super.drawFrame
La méthode complètera la mise en page, la peinture et d'autres travaux en fonction des informations de l'arborescence de rendu. La logique spécifique est indiquée dans la liste de codes 5-39. finalizeTree
Il sera exécuté une fois le travail de rendu du cadre du thread de l'interface utilisateur terminé et il est principalement responsable du nettoyage des nœuds inutiles de l'arborescence des éléments. La logique pertinente sera analysée en détail plus tard.
// 代码清单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;
}
}
Dans la logique ci-dessus, flushLayout
la méthode est chargée de mettre à jour les informations de taille (taille) et de position (décalage) de chaque nœud dans l'arborescence de rendu ; flushPaint
la méthode est chargée de traverser l'arborescence de rendu, d'exécuter Paint
la logique de chaque nœud et de générer un Arborescence des couches. compositeFrame
La méthode est responsable de la construction Scene
de l'objet à partir de l'arborescence des couches et complètera la logique de rendu finale des données d'image via le moteur.
Ce qui précède est le mécanisme Vsync de Flutter : lorsqu'une requête est envoyée via le Framework, le moteur enregistre la requête auprès de l'API Embedder, et lorsque le signal Vsync arrive, le moteur rappelle le Framework. Pendant cette période, il commencera par passez du thread d'interface utilisateur au thread de plate-forme, puis basculez du thread de plate-forme. Retournez au thread d'interface utilisateur.
Analyse de la conception de la continuation
Dans le Listing 5-34, il y a un morceau de logique très obscur qui n'est pas analysé, c'est-à-dire producer_continuation_
parce qu'il n'est pas simplement compréhensible à travers le contexte de la méthode. Après l'arrivée du signal Vsync, les données d'un cadre passent par différentes étapes telles que Build, Layout, Paint, etc., et lorsque le rendu démarre réellement, l'objet existera toujours. Si l'interprétation est dispersée, il est très il est probable que le pipeline de rendu ne puisse pas être aperçu car cet objet est ignoré. L'ensemble de l'image, donc cette section l'analysera séparément.
layer_tree_pipeline_
L'objet du Listing 5-34 Animator
est initialisé dans le constructeur et Produce
la méthode est appelée avant le rendu.La logique est présentée dans le Listing 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()};
}
Étant donné que le pipeline de rendu implique plusieurs threads, le nombre d'images à rendre est contrôlé par un sémaphore empty_
. La dénomination ci-dessus Continuation
signifie que la tâche de rendu d'une image existe actuellement mais n'a pas encore été terminée. Parce qu'après l'arrivée du signal Vsync, il est déterminé à rendre une image, de sorte que Produce
la marque est complétée immédiatement tout au long du processus.L'avantage de ceci est que le signal Vsync et le rendu final de l'image sont un à un. En raison de l'existence du sémaphore, il n'y aura pas de situation où un signal Vsync provoque un rendu multi-images, car le producteur et le consommateur sont en correspondance biunivoque.
Une fois la logique de rendu de la couche Framework terminée, les données d'une image sont complètement prêtes. À ce stade, l' producer_continuation_
objet généré dans le Listing 5-34 peut être notifié. Pour être précis, les données à rendre sont soumises. La logique de soumission spécifique consiste à appeler ProducerCommit
la fonction liée au contenu précédent, comme indiqué dans le Listing 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;
}
Notez que la logique ci-dessus soumet simplement les données à rendre ( resource
) à layer_tree_pipeline_
la file d'attente d'images de l'objet à rendre, pourquoi ne pas les rendre directement ici ? La première est que le thread d'interface utilisateur actuel ne peut toujours pas effectuer le rendu ; l'autre est que le travail de préparation avant le rendu n'a pas été terminé, il est donc temporairement stocké dans la file d'attente. available_
Le rôle du champ ici empty_
est similaire à celui du champ, qui contrôlent tous deux le nombre d'images à restituer.
Une fois le travail lié au rendu entièrement préparé, Rasterizer
la Draw
méthode consommera les données des images à rendre dans la file d'attente, comme indiqué dans le Listing 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帧待渲染,完成
}
Étant donné que le rendu est Raster
effectué dans le thread, le verrou est ici très nécessaire.Le cœur de la logique ci-dessus consiste à extraire les layer_tree_pipeline_
premières données de la file d'attente de trames de l'objet à rendre et consumer
à les consommer via la fonction.
En général, Continuation
l'existence de résout les deux problèmes suivants.
-
Correspondance un à un entre les signaux Vsync et les trames à restituer (sémaphore).
-
Problèmes de synchronisation (verrous) entre la production de données de thread d'interface utilisateur et la consommation de données de thread raster.
De plus, le suivi est fourni Continuation
par le rendu de chaque image. trace_id
Bien que producer_continuation_
les points d'appel soient très dispersés, du point de vue de la conception, Continuation
le découplage des fonctions est garanti. Comme le montre la figure 5-10, après l'arrivée du signal Vsync, Animator
une Continuation
instance est générée. Le framework et le moteur complètent la synthèse d'une trame de données sur le thread d'interface utilisateur et Continuation
la soumettent au layer_tree_pipeline_
champ via l'objet, Rasterizer
puis la lisent pendant le processus réel. Le rendu et le niveau sont très bons.
Dans la Figure 5-10, pipeline
le rôle joué est en fait dans Android BufferQueue
, c'est-à-dire le producteur (Framework) et le consommateur (Rasterizer) qui connectent les données de rendu.
Principe du dessin flottant
Il existe trois objets liés au dessin dans Flutter, à savoir Canvas
, Layer
et Scene
:
Canvas
: Encapsule diverses instructions de dessin de Flutter Skia, telles que dessiner des lignes, dessiner des cercles, dessiner des rectangles et d'autres instructions.Layer
: divisé en deux types : classe de conteneur et classe de dessin ; pour le moment, il peut être compris comme le support du produit de dessin. Par exemple, après avoir appelé l'API de dessin de ,Canvas
le produit de dessin correspondant est enregistré dansPictureLayer.picture
l'objet.Scene
: L'élément à afficher à l'écran. Avant de télécharger à l'écran, nous devons associer les produits de dessin enregistrés dans Layer avecScene
.
Processus de dessin Flutter :
-
Construisez-en un
Canvas
pour le dessin ; en même temps, vous devez également créer un enregistreur d'instructions de dessin, car l'instruction de dessin est finalement transmise àSkia
, etCanvas
peut initier en continu plusieurs instructions de dessin, et l'enregistreur d'instructions est utilisé pour collecterCanvas
toutes les instructions de dessin dans un période de temps , doncCanvas
le constructeur doit passer une instance comme premier paramètrePictureRecorder
. -
Canvas
Une fois le dessin terminé,PictureRecorder
faites passer le produit de dessin et enregistrez-le au formatLayer
. -
Construisez
Scene
l'objetlayer
et associez le dessin produit deScene
avec . -
Sur l'écran, appelez
window.render
l'API pourScene
envoyer le produit dessiné à l'écran au GPU.
Ci-dessous, nous utilisons un exemple pour démontrer l'ensemble du processus de dessin :
Vous souvenez-vous encore de l'exemple de dessiner un échiquier auparavant ? Qu'il ait été passé CustomPaint
ou personnalisé RenderObject
, il a été dessiné sous le modèle de cadre Widget de Flutter. En fait, à la fin, Flutter suivra le processus mentionné ci-dessus pour terminer le dessin à la couche inférieure. Dans ce cas, nous pouvons également main
appeler directement ces API de bas niveau dans la fonction pour terminer. Montrons comment main
dessiner l'échiquier à l'écran directement dans la fonction.
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);
}
résultat courant :
Image
PictureLayer
Le produit de dessin dont nous avons parlé plus haut est Picture
qu'il y a deux points à clarifier Picture
:
Picture
En fait, il s'agit d'une série d'instructions d'opération de dessin graphique, qui peuvent faire référence aux Picture
commentaires du code source de la classe.
Picture
Pour être affiché à l'écran, il doit être pixellisé, puis Flutter mettra en cache les informations bitmap pixellisées, c'est-à-dire que l'instruction de Picture
dessin pour le même objet ne sera exécutée qu'une seule fois, et le bitmap dessiné sera mis en cache.
En combinant les deux points ci-dessus, nous pouvons voir PictureLayer
que le "produit de dessin" est une série d'"instructions de dessin" au début. Lorsque le premier dessin est terminé, les informations bitmap seront mises en cache et les instructions de dessin ne seront plus exécutées . , donc le "produit de dessin" est un bitmap pour le moment. Pour des raisons de compréhension, nous pouvons y penser plus tard comme faisant référence au bitmap dessiné.
Bitmap à l'image dessinée par Canvas
Puisqu'il Picture
enregistre le produit de dessin, il devrait également fournir une méthode pour exporter le produit de dessin. En fait, Picture
il existe une toImage
méthode pour exporter en fonction de la taille spécifiée 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);
Couche
Réfléchissons maintenant à une question : Layer
quel est le rôle du détenteur du produit de dessin ? La réponse est:
frame
Les artefacts de dessin peuvent être réutilisés entre différents (si aucun changement ne se produit).- Divisez le contour du dessin et réduisez la plage de redessin .
Layer
Les classes clés et leurs relations d'héritage sont les suivantes :
Dans la Figure 5-14, Layer
il s'agit Layer Tree
de la classe de base de tous les nœuds de , et ses sous-classes sont principalement divisées en trois types.
- Le premier est
ContainerLayer
, comme son nom l'indique, c'est le conteneur d'autresLayer
nœuds, commeOpacityLayer
l'ajout d'un effet de transparence aux nœuds enfants etClipRectLayer
la coupe des nœuds enfants. Nous appelonsContainerLayer
la couche directement héritée de la classe la classe de conteneur Layer , et la classe de conteneur Layer peut ajouter n'importe quel nombre de sous-couches. - Le deuxième type est
PictureLayer
la couche qui enregistre le produit de dessin, quiLayer
est le nœud responsable de l'exécution du dessin réel. Ce nœud_picture
contient unui.PictureRecorder
objet à travers le champ, qui est utilisé par le moteur pour enregistrer les instructions de dessin correspondantes. Nous nous référons au Layer qui peut directement transporter (ou associer) le résultat du dessin en tant que classe de dessin Layer . - Le troisième type est
TextureLayer
etPlatformViewLayer
, leurs sources de rendu seront fournies en externe etLayer
incorporées dans le rendu de trame de Flutter.
De plus, PaintingContext
c'est Layer
le contexte du dessin, fournissant Canvas
l'objet pour le dessin final. ui.EngineLayer
C'est Layer
la représentation dans le moteur du framework Flutter, et sa structure Layer
est presque la même que celle du framework (c'est-à-dire la figure 5-14), donc je ne la répéterai pas ici.
Le rôle de la classe de conteneur Layer
Le concept de la classe de conteneur Layer présenté ci-dessus, quelles sont ses fonctions et ses scénarios d'utilisation spécifiques ?
-
Composez la structure de dessin de l'arborescence des composants dans un arbre .
Étant donné que le widget dans Flutter est une structure arborescente, la
RenderObject
structure de dessin correspondante doit également être une structure arborescente. Flutter générera une arborescence de couches pour l'arborescence des composants selon certaines "règles spécifiques", et la classe de conteneur Layer peut former une structure arborescente ( le calque parent peut contenir n'importe quel nombre de calques enfants et le calque enfant peut contenir n'importe quel nombre de calques enfants). -
Certains effets de transformation peuvent être appliqués collectivement à plusieurs calques .
La classe de conteneur Layer peut effectuer certains effets de transformation sur ses sous-calques dans leur ensemble, tels que l'effet d'écrêtage (
ClipRectLayer、ClipRRectLayer、ClipPathLayer
), l'effet de filtrage (ColorFilterLayer、ImageFilterLayer
), la transformation de matrice (TransformLayer
), la transformation de transparence (OpacityLayer
), etc.
Bien qu'il ContainerLayer
ne s'agisse pas d'une classe abstraite, les développeurs peuvent créer directement une instance de la classe, mais en fait ils le font rarement. Au contraire, ils peuvent utiliser directement ses sous-classesContainerLayer
lorsqu'elles doivent être utilisées . Si nous n'en avons vraiment pas besoin effets de transformation, puis utilisez , ne vous inquiétez pas de la surcharge de performances supplémentaire, son implémentation sous-jacente (dans Skia) est très efficace.ContainerLayer
OffsetLayer
Classe de dessin Calque
Concentrons-nous sur PictureLayer
la classe ci-dessous, qui est la classe de dessin Layer la plus couramment utilisée dans Flutter .
Nous savons que l'affichage final à l'écran correspond aux informations bitmap et que les informations bitmap sont Canvas
dessinées par l'API. En fait, Canvas
le produit de dessin de est Picture
la représentation de l'objet, et ce n'est que dans la version actuelle de Flutter qu'il PictureLayer
a l'objet.En d'autres termes, le résultat du dessin du composant qui se dessine lui-même et ses nœuds enfants picture
dans Flutter finira par tomber dans le .Canvas
PictureLayer
Le choix de la méthode de réalisation de l'effet de transformation
Comme mentionné ci-dessus , certaines transformations ContainerLayer
peuvent être effectuées sur ses layer
sous-ensembles. En fait, la plupart Canvas
des API du système d'interface utilisateur ont également des API liées à la transformation, ce qui signifie que certains effets de transformation peuvent être ContainerLayer
réalisés de bout en Canvas
bout. Par exemple, pour implémenter la transformation de la traduction, nous pouvons utiliser OffsetLayer
ou utiliser directement Canva.translate
l'API. Cela étant, quel est le principe de notre choix d'implémentation ?
Maintenant, commençons par comprendre le principe de la classe de conteneur Layer pour obtenir l'effet de transformation. Skia
La transformation de la classe de conteneur Layer est implémentée via la couche inférieure et n'a pas besoin Canvas
d'être traitée. Le principe spécifique est que la classe conteneur Layer avec fonction de transformation correspondra à une Skia
dans le moteur Layer
.Afin de la distinguer de la Layer dans le framework Flutter, la dans Flutter Skia
est Layer
appelée engine layer
. Et la classe de conteneur Layer avec fonction de transformation Scene
en construira une avant de l'ajouter à engine layer
, prenons comme OffsetLayer
exemple pour voir son implémentation associée :
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
La fonction de transformation de décalage pour ses nœuds enfants dans son ensemble est Skia
prise en charge par l'implémentation dans . Skia
Il peut prendre en charge le rendu multicouche, mais cela ne signifie pas que plus il y a de couches, mieux c'est, engineLayer
cela prendra une certaine quantité de ressources. La bibliothèque de composants intégrée de Flutter qui implique des effets de transformation est d'abord utilisée Canvas
pour y parvenir Canvas
. très difficile ou impossible à réaliser sera ContainerLayer
implémenté en utilisant .
Donc, dans quels scénarios Canvas
serait-il très difficile d'obtenir l'effet de transformation et doit-il être ContainerLayer
atteint ? Un scénario typique est que nous devons transformer un certain sous-arbre dans l'arborescence des composants dans son ensemble, et il y en a plusieurs dans le sous-arbre PictureLayer
. En effet, un Canvas
correspond généralement à un PictureLayer
, et différents Canvas
s sont isolés les uns des autres.Ce n'est que Canvas
lorsque tous les composants du sous-arbre sont dessinés à travers le même que la Canvas
transformation globale de tous les nœuds enfants peut être effectuée via le , sinon il ne peut être passé que ContainerLayer
.
Remarque :
Canvas
Il existe également...layer
une API associée nommée dans l'objet, par exempleCanvas.saveLayer
, elle a une signification différente de celle présentée dans cette sectionLayer
.Canvas
L'objetlayer
est principalement de fournir un moyen de mettre en cache les résultats de dessin intermédiaires pendant le processus de dessin. Il est conçu pour faciliter le dessin séparé entre plusieurs éléments de dessin lors du dessin d'objets complexes. Pour plus d'informations sur les API associées, veuillez vous référer aux documents associés. Nous pouvons toutCanvas layer
simplement pensez que peu importeCanvas
combien de paires sont crééeslayer
, ces objetslayer
sont tous sur le même objetPictureLayer
.
exemple
Regardons un bout de code pour comprendre Layer
à quoi il correspond :
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();
});
}
L'arborescence de rendu et l'arborescence des couches correspondant au code ci-dessus sont illustrées dans la figure :
Il y a donc en fait 4 arbres dans Flutter, c'est-à-dire Layer Tree L'avantage d'utiliser Layer Tree est qu'il peut faire une mise à jour partielle du processus de peinture (oui, l'idée de mise à jour partielle dans Flutter est omniprésente), comme lors de la lecture d'une vidéo. Les commandes telles que le bouton "Lecture" et la barre de progression n'ont pas besoin d'être peintes à chaque image. De plus, la liste de Flutter utilise Layer Tree pour obtenir un glissement efficace : dans la liste de Flutter, chaque élément a un calque indépendant, de sorte que lors du glissement, seules les informations de localisation du calque doivent être mises à jour sans redessiner le contenu.
Processus de dessin de l'arborescence des composants
L'implémentation liée au dessin se trouve dans l'objet de rendu RenderObject
et RenderObject
les principaux attributs liés au dessin sont :
layer
isRepaintBoundary
(bool
genre)needsCompositing
(bool
genre)
dessiner des nœuds de bordure
Nous nous référons aux nœuds avec isRepaintBoundary
une valeur d'attribut en tant que nœuds de bordure de dessin.true
RenderObject
Flutter est livré avec un RepaintBoundary
composant, sa fonction est d'insérer un nœud de frontière de dessin dans l'arborescence des composants.
besoinsCompositing
Dans l'arborescence de rendu, chaque RenderObject
objet a un needsCompositing
attribut, qui est utilisé pour juger si lui et ses nœuds enfants ont une couche à synthétiser (si c'est le cas, cela true
signifie qu'il a une couche indépendante), et il y a aussi un _needsCompositingBitsUpdate
champ pour Flags si cette propriété doit être mise à jour. Avant le début de Paint, Flutter terminera d'abord needsCompositing
la mise à jour des propriétés, puis commencera le dessin formel.
Parlons d'abord du processus général des arborescences de composants de dessin Flutter. Notez que ce n'est pas un processus complet, car nous ignorerons temporairement le besoin de "composition de couches" ( ) dans le sous-arbre , dont nous parlerons plus tard Compositing
. Voici le processus général :
Lorsque Flutter dessine pour la première fois, il dessine les nœuds enfants de manière récursive de haut en bas.Chaque fois qu'il rencontre un nœud frontière, il jugera que si la propriété du nœud frontière est vide (le type est ), un nouveau layer
sera ContainerLayer
créé OffsetLayer
et attribué Donnez-le ; s'il n'est pas vide, utilisez-le directement. Ensuite, le nœud frontière est layer
passé aux nœuds enfants, puis il y a deux cas :
- Si le nœud enfant est un nœud non-frontière et doit être dessiné, lors du premier dessin :
1) Créez unCanvas
objet et unPictureLayer
, puis liez-les, et les appels ultérieursCanvas
à draw tomberontPictureLayer
sur la liaison.
2)PictureLayer
Ajoutez ensuite ceci au nœud de frontièrelayer
. - Si ce n'est pas la première fois que vous dessinez, réutilisez les objets
PictureLayer
et existantsCanvas
. - Si le nœud enfant est un nœud frontière, le processus ci-dessus est récursif pour le nœud enfant. Lorsque la récursivité du sous-arbre est terminée, les nœuds enfants sont
layer
ajoutés au parentLayer
.
Une fois l'ensemble du processus exécuté, un arbre des couches est généré . Ci-dessous, nous utilisons un exemple pour comprendre l'ensemble du processus : le côté gauche de la figure ci-dessous est l'arborescence des widgets, et le côté droit est l'arborescence des couches générée.
Jetons un coup d'œil au processus de génération :
RenderView
C'est le nœud racine de l'application Flutter, et le dessin partira de celui-ci, car il s'agit d'un nœud de frontière de dessin.Lorsque vous dessinez pour la première fois, un sera créé pour lui, nous le marquons comme, puis leOffsetLayer
passonsOffsetLayer1
àOffsetLayer1
leRow
.- 由于
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
的绘制产物。- Ensuite, après
Row
avoir terminéColumn1
le dessin de , commencez à dessiner le deuxième nœud enfantRepaintBoundary
et passez-le à , puisqu'il s'agit d'un nœud de frontière de dessin etRow
que c'est le premier dessin, il en créera un pour lui , puis le passera à , et La différence est que va utiliser pour dessiner et , le processus de dessin est le même que Column1, donc je n'entrerai pas dans les détails ici.OffsetLayer1
RepaintBoundary
OffsetLayer2
RepaintBoundary
OffsetLayer2
Column2
Column1
Column2
OffsetLayer2
Text3
Text4
- Lorsque
RepaintBoundary
les nœuds enfants de sont dessinés,RepaintBoundary
lelayer
(OffsetLayer2
) de est ajouté au parentLayer
(OffsetLayer1
).
Jusqu'à présent, l'intégralité de l'arborescence des composants a été dessinée et une arborescence des couches affichée à droite est générée. Ce qu'il faut dire, c'est que PictureLayer1
vous et OffsetLayer2
êtes frères, et qu'ils sont tous les deux OffsetLayer1
vos enfants. Grâce à l'exemple ci-dessus, nous pouvons au moins trouver une chose : la même chose Layer
peut être partagée par plusieurs composants, tels que Text1
et Text2
shared PictureLayer1
.
Attendez, s'il est partagé, cela causera-t-il un problème, par exemple Text1
lorsque le texte change et doit être redessiné, devra-t-il également Text2
être redessiné ?
La réponse est oui! Cela semble un peu déraisonnable, alors pourquoi le partager ? Chaque composant ne peut-il pas être dessiné sur un Layer
? Cela évite également les interférences mutuelles. La raison est en fait d'économiser des ressources, Layer
un trop grand nombre Skia
consommera plus de ressources, il s'agit donc en fait d'un compromis.
Encore une fois, ce qui précède n'est qu'un flux général de dessin. En général, le nombre et la structure de ContainerLayer
et dans l'arborescence des couches PictureLayer
sont en correspondance un à un avec les nœuds de frontièreWidget
dans l'arborescence Notez qu'ils ne sont pas en correspondance un à un. Bien sûr, s'il y a de nouveaux sous-composants dans l'arbre qui sont ajoutés pendant le processus de dessin , alors il y aura plus que le nombre de nœuds de frontière, et ce n'est pas une correspondance un à un pour le moment. De plus, lors de la mise en œuvre de nombreux composants avec des effets tels que la transformation, l'écrêtage et la transparence dans Flutter, de nouveaux calques seront ajoutés à l'arborescence des calques.Widget
Widget
Layer
Layer
Marquer la marque d'étapeBesoinsRepeindre
RenderObject
La demande de rafraîchissement est markNeedsRepaint
initiée en appelant Avant d'introduire markNeedsRepaint
ce qu'il fait spécifiquement, devinons ce qu'il devrait faire en fonction du processus de dessin Flutter décrit ci-dessus.
Nous savons qu'il y a un partage de calque dans le processus de dessin , donc lors du redessin, tous Layer
les composants qui partagent le même doivent être redessinés. Par exemple, dans l'exemple ci-dessus, Text1
s'il y a un changement, alors nous devons Text1
redessiner en plusText2
; s'il Text3
y a un changement, alors nous devons également redessinerText4
; comment y parvenir ?
Car qu'est -ce qui est partagé par Text1
et est , et qui est propriétaire de ? Trouvez-le et laissez-le le redessiner! OK, on peut facilement trouver que le propriétaire de est le nœud racine , qui est également le premier parent de et dessine le nœud de bordure. De même, c'est aussi le premier parent de et pour dessiner le nœud de bordure, nous pouvons donc tirer une conclusion : lorsqu'un nœud doit être redessiné, nous devons trouver le premier parent le plus proche pour dessiner le nœud de bordure, puis Just laissez-le redessiner et terminez simplement ce processus, lorsqu'un nœud l'appelle, les étapes spécifiques sont les suivantes :Text2
OffsetLayer1
OffsetLayer1
OffsetLayer1
RenderView
Text1
Text2
OffsetLayer2
Text3
Text4
markNeedsRepaint
- Il recherchera du nœud actuel au parent jusqu'à ce qu'il trouve un nœud de contour de dessin, puis il ajoutera le nœud de contour de dessin à sa
PiplineOwner
liste_nodesNeedingPaint
(enregistrez le nœud de contour de dessin qui doit être redessiné). - Au cours du processus de recherche, il
_needsPaint
définira les propriétés de tous les nœuds sur le chemin de lui-même au nœud de frontière de dessin surtrue
, indiquant qu'il doit être redessiné. - Demandez-en un nouveau
frame
, exécutez le processus de redessiner.
markNeedsRepaint
Le code source principal supprimé est le suivant :
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();
}
}
Il convient de mentionner que dans la version actuelle de Flutter, il n'ira jamais à la dernière else
branche, car le nœud racine dans la version actuelle est un RenderView
, et la propriété du composant isRepaintBoundary
est true
, donc s'il est appelé, renderView.markNeedsPaint()
il ira à la branche pour isRepaintBoundary
.true
Après avoir demandé une nouvelle trame, drawFrame
le processus se poursuivra à l'arrivée de la trame suivante. Rappelez-vous cette méthode :
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
Les trois fonctions liées à la neutralisation et au dessin impliquent flushCompositingBits
, flushPaint
et compositeFrame
, et le processus de redessin est dans flushPaint
, alors concentrons-nous flushPaint
d'abord sur le processus. À propos de flushCompositingBits
, il implique Layer
la synthèse dans l'arbre des composants, que nous présenterons plus tard.
Flush stage flushPeinture
flushPaint
Méthodes comme ci-dessous :
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(); } // 结束绘制
}
}
La logique ci-dessus appellera les méthodes séquentiellement à partir du nœud le plus profond par ordre de profondeur repaintCompositedChild
. Il convient de noter que les premières étapes commencent le traitement à partir du nœud avec la plus petite profondeur, mais Paint
l'étape doit commencer à partir du nœud avec la plus grande profondeur , car l'effet du nœud ancêtre Paint
doit être appliqué au nœud enfant, tel qu'un nœud de découpage, au nœud enfant Pour produire un effet de découpage, vous devez attendre que les nœuds enfants aient fini de dessiner . PaintingContext
La méthode de repaintCompositedChild
appellera éventuellement _repaintCompositedChild
la méthode.
Ici, nous devons rappeler une chose, nous stateState
avons dit dans la section sur le processus d'introduction que lorsqu'un nœud dans l'arborescence des composants veut se mettre à jour, il appellera markNeedsRepaint
une méthode, et la méthode recherchera à partir du nœud actuel jusqu'à ce qu'il trouve un nœud isRepaintBoundary
c'est-à-dire true
, puis ce sera Le nœud est ajouté à nodesNeedingPaint
la liste. Par conséquent, nodesNeedingPaint
les nœuds dans isRepaintBoundary
doivent être true
, en d'autres termes, les nœuds qui peuvent être ajoutés à nodesNeedingPaint
la liste sont tous des frontières de dessin , alors comment fonctionne cette frontière, continuons à regarder PaintingContext._repaintCompositedChild
l'implémentation de la fonction.
// 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(); // 停止记录工作
}
On peut voir que lors du dessin d'un nœud frontière, il vérifiera d'abord s'il existe layer
, et si ce n'est pas le cas, un nouveau sera créé OffsetLayer
pour lui.
Un objet (noté ) est alors offsetLayer
construit à partir de ,PaintingContext
childContext
Deuxièmement, childContext
il est passé par des paramètres, généralement null
, donc un nouvel PaintingContext
objet sera créé pour dessiner le calque. Ensuite, le sous-composant en créera un lorsqu'il obtiendra l' objet context
, puis créera un objet à associer au nouvellement créé , ce qui signifie que les produits suivants dessinés à travers le même appartiennent au même .canvas
PictureLayer
Canvas
PictureLayer
childContext
canvas
PictureLayer
_paintWithContext
La méthode est principalement responsable du dessin de la couche de nœud enfant actuelle.
Enfin, une fois que tous les nœuds enfants ont été dessinés, stopRecordingIfNeeded
la méthode est appelée pour arrêter l'enregistrement du travail pour l' PaintingContext
objet actuel (c'est-à-dire . childContext
(Framework est responsable de l'enregistrement de diverses instructions de dessin, et le vrai travail de dessin est effectué dans Engine)
Le code de la méthode est le _paintWithContext
suivant :
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) {
}
Dans la logique ci-dessus, marquez d'abord _needsPaint
le champ comme false
, car le dessin est sur le point de commencer et l'opération de dessin spécifique est déterminée par la méthode RenderObject
implémentée par la sous-classe paint
. Cette méthode doit être implémentée par le nœud lui-même pour se dessiner. Différents types de nœuds ont généralement des algorithmes de dessin différents, mais les fonctions sont similaires, c'est-à-dire : s'il s'agit d'un composant conteneur, il doit dessiner les enfants et lui-même (bien sûr, le conteneur lui-même peut ne pas avoir de logique de dessin. , dans ce cas, dessinez uniquement l'enfant, tel que le composant Center), s'il ne s'agit pas d'un composant de conteneur, dessinez lui-même (tel que Image).
A titre RenderView
d'exemple, il finira par appeler paintChild
la méthode, le code est le suivant :
// 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);
}
}
Sa logique principale est la suivante : si le nœud courant est un nœud frontière, arrêter le dessin du calque courant, _compositeChild
démarrer le dessin du nœud courant en créant un nouveau calque, sinon, appeler la _paintWithContext
méthode précédemment analysée pour commencer à exécuter Paint
la logique basée sur le couche courante.
Parmi eux, _compositeChild
la méthode est la suivante :
// 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); // 后面分析
}
La logique ci-dessus appellera d'abord la méthode analysée précédemment repaintCompositedChild
pour créer un nouveau calque, et synchronisera les informations du calque offset
, c'est-à-dire là où le calque commence à dessiner. Enfin, appelez appendLayer
la méthode pour ajouter le nouveau calque à l'arbre des calques actuel.
Il y a trois points à noter ici :
- Lors du dessin d'un nœud enfant, si un nœud frontière est rencontré et qu'il n'a pas besoin d'être redessiné (
_needsPaint
forfalse
), il réutilisera directement le nœud frontièrelayer
sans redessiner ! C'est le principe selon lequel les nœuds frontières peuvent être réutilisés à travers les trames. - Comme le type du nœud limite
layer
estContainerLayer
, il est possible d'y ajouter des nœuds enfants. - Notez que le nœud limite actuel
layer
est ajouté au nœud limite parent , et non au nœud parent.
_containerLayer
Les méthodes des champs sont append
les suivantes :
// 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);
}
La logique ci-dessus consiste principalement à terminer le montage des nœuds enfants. Dans adoptChild
la méthode, étant donné que la plupart des alwaysNeedsAddToScene
attributs des nœuds Layer sont false
, la méthode sera appelée markNeedsAddToScene
pour indiquer la construction que le nœud actuel doit joindre Scene
. Scene
C'est le produit final de la synthèse Layer Tree.
Une fois l'exécution du processus ci-dessus terminée, tous les nœuds de bordure layer
seront finalement connectés pour former un arbre de couches.
Analyse de la méthode de peinture de RenderColoredBox et RenderOpacity
Afin d'approfondir Paint
la compréhension du nœud, les deux méthodes RenderObject
de nœud typiques suivantes paint
sont analysées, la première est _RenderColoredBox
la paint
méthode, comme indiqué dans la liste de codes 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); }
}
La logique ci-dessus dessinera d'abord la couleur de la zone cible, puis dessinera les nœuds enfants. La RenderOpacity
méthode paint
est plus compliquée, comme le montre le Listing 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
Il ne sera dessiné que lorsque le nœud enfant existe, et il sera renvoyé directement lorsqu'il est entièrement transparent, et il sera dessiné directement dans l' PaintContext
objet courant lorsqu'il est opaque, et il sera dessiné pushOpacity
dans une méthode uniquement lorsqu'il est semi-transparent.OpacityLayer
Pour les cas semi-transparents, la méthode qui sera finalement appelée PaintingContext
est pushOpacity
indiquée dans le Listing 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;
}
La logique ci-dessus utilisera le nœud actuel ( RenderOpacity
) Layer
, sinon, créez-en un nouveau OpacityLayer
, définissez sa transparence, sa valeur de décalage et d'autres propriétés, puis Layer
ajoutez-le à l'arborescence des couches, comme indiqué dans le Listing 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);
}
Dans la logique ci-dessus, la première étape consiste à supprimer tous les nœuds enfants de la couche actuelle. Étape 2 : arrêtez le dessin du calque actuel, comme indiqué dans l'extrait de code 5-78, puis appelez appendLayer
la méthode pour ajouter le calque actuel à l'arborescence des couches. La logique de base est illustrée dans l'extrait de code 5-73. Étape 3 : créez un nouvel PaintingContext
objet pour dessiner un nouveau calque, qui est transmis painter
par un RenderObject
nœud spécifique, et RenderOpacity
sa painter
méthode est illustrée dans le Listing 5-79. Enfin, terminez le dessin du calque actuel et quittez.
// 代码清单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;
}
La logique ci-dessus est utilisée pour terminer le dessin d'une couche, principalement pour définir les champs pertinents sur null
, et endRecording
la méthode appellera éventuellement une méthode dans le moteur.
Ensuite, analysez OpacityLayer
la logique de dessin réelle, comme indiqué dans le Listing 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
}
}
La logique ci-dessus consiste principalement à dessiner des nœuds enfants, et paintChild
la logique de la méthode a été introduite plus tôt. RenderOpacity
Il est uniquement chargé de fournir un calque avec un effet transparent, et il RenderParagraph
doit Canvas
être dessiné selon get
la méthode indiquée dans le Listing 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!);
}
À partir de la logique ci-dessus, nous pouvons voir que le dessin réel est PictureLayer
effectué dans , PictureRecorder
qui est responsable de la sauvegarde de toutes les instructions de dessin.
Il convient de noter que la logique ci-dessus Canvas
, PictureRecorder
etc. sont toutes héritées de la classe, qui est la classe parente NativeFieldWrapperClass2
fournie par Dart pour encapsuler des objets (C++), de sorte que les opérations de dessin ci-dessus (par , par enregistrement) sont des appels, et ces appels will est synthétisé dans les données d'écran finales.Native
Canvas
PictureRecorder
Native
Créer un nouveau calque d'image
Maintenant, sur la base de l'exemple au début de cette section, nous ajoutons un troisième nœud enfant à Row Text5
, comme indiqué sur la figure, alors à quoi ressemblera son arborescence de couches ?
Étant donné que Text5
est dessiné une fois le dessin terminé, que s'est-il passé après que le ( ) de a été ajouté au parent ( ) RepaintBoundary
dans l'exemple ci-dessus lorsque les RepaintBoundary
nœuds enfants de ont été dessinés ? La réponse se trouve dans la dernière ligne de ce que nous avons présenté ci-dessus :RepaintBoundary
layer
OffsetLayer2
Layer
OffsetLayer1
repaintCompositedChild
...
childContext.stopRecordingIfNeeded();
Regardons son code de base après suppression :
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
组件)的场景中就会有问题,下面我们看一个例子,结构图如下:
Sur la gauche se trouve une Stack
mise en page et sur la droite se trouve la structure correspondante de l'arborescence des calques ; nous savons que Stack
la mise en page s'affichera en cascade en fonction de l'ordre dans lequel ses sous-composants sont ajoutés, le premier enfant ajouté est en bas et le dernier enfant ajouté est en haut. On peut imaginer que s'il Child3
est réutilisé lors du dessin PictureLayer1
, il sera recouvert Child3
par Child2
, ce qui ne répond évidemment pas aux attentes, mais si vous en créez un nouveau et que vous PictureLayer
l'ajoutez à OffsetLayer
la fin, vous pouvez obtenir le résultat correct.
Maintenant, réfléchissons davantage : si Child2
le nœud parent de n'est pas RepaintBoundary
, cela signifie-t-il que Child3
and Child1
peut partager la même chosePictureLayer
?
- la réponse est négative ! Si
Child2
le composant parent de est remplacé par un composant personnalisé, dans ce composant personnalisé, nous souhaitons apporter des modifications de matrice aux nœuds enfants lors du rendu. Pour réaliser cette fonction, nous en créons un nouveau et spécifions les règles de transformation, puis nousTransformLayer
mettons Il est passé àChild2
,Child2
et une fois le dessin terminé, nous devonsTransformLayer
l'ajouter àLayer
l'arborescence (il ne sera pas affiché s'il n'est pas ajouté àLayer
l'arborescence), puis l'arborescence des composants et la structure finale de l'arborescence des couches sont affichées dans la figure :
RepaintBoudary
On peut constater que cette situation est essentiellement la même que celle utilisée ci-dessus Child3
et ne doit pas être réutilisée PictureLayer1
, nous pouvons donc maintenant résumer une règle générale : tant qu'un composant doit ajouter un nouveau calque à l'arborescence des calques, il doit également Terminer le dessin du PictureLayer actuel. C'est pourquoi PaintingContext
les méthodes qui doivent ajouter une nouvelle couche à l'arborescence des couches dans (par exemple pushLayer
, addLayer
) ont les deux lignes de code suivantes :
stopRecordingIfNeeded(); //先结束当前 PictureLayer 的绘制
appendLayer(layer);// 再添加到 layer树
Il s'agit d'un ajout standard à l'arborescence des calques Layer
. Cette conclusion doit être gardée à l'esprit, et nous flushCompositingBits()
l'utiliserons dans le principe présenté plus loin.
En résumé, la structure finale de l'arborescence des couches est à peu près celle illustrée sur la figure (juste un exemple, qui ne correspond pas à cet exemple) :
cadre composite
Une fois créé layer
, il doit être affiché à l'écran et cette partie du travail est renderView.compositeFrame
effectuée par la méthode. En fait, sa logique de mise en œuvre est très simple : d'abord via layer
la construction Scene
, et enfin via window.render
l'API pour rendre :
void compositeFrame() {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer!.buildScene(builder);
window.render(scene);
...
}
Ce qui mérite d'être mentionné ici, c'est le Scene
processus de construction, jetons un coup d'œil au code source principal :
ui.Scene buildScene(ui.SceneBuilder builder) {
updateSubtreeNeedsAddToScene();
addToScene(builder); //关键
final ui.Scene scene = builder.build();
return scene;
}
La ligne la plus critique est l'appel . La fonction principale de cette méthode est de passer addToScene
chaque couche dans l'arborescence des couches (l'API sera éventuellement appelée . Si vous souhaitez en savoir plus, il est recommandé de vérifier les méthodes de et ), c'est la dernière action préparatoire avant de passer à l'écran, la dernière est d'appeler pour envoyer les données de dessin au GPU pour le rendu ! ( La méthode du moteur de calque natif sera appelée pour effectuer le travail de rendu, et l'analyse d'expansion ne sera pas effectuée ici)layer
Skia
native
OffsetLayer
PictureLayer
addToScene
window.render
window.render
Render
Cas d'utilisation de la couche
Cette section vous montre comment l'utiliser dans des composants personnalisés en optimisant l'"exemple de dessin d'échiquier" précédent Layer
.
Réaliser le cache de dessin via Layer
Notre exemple précédent de dessin d'un échiquier est le composant utilisé CustomPaint
, puis la méthode de dessin de l'échiquier et des pièces d'échecs est mise en œuvre en même temps. En fait, il peut y avoir une optimisation ici, car l'échiquier ne changera pas, donc l'idéal Lorsque la zone de dessin ne change pas painter
,paint
Remarque : En développement réel, pour réaliser les fonctions ci-dessus, il est préférable d'utiliser la méthode « Widget combination » suggérée par Flutter : par exemple, dessinez l'échiquier et les pièces d'échecs en deux respectivement , puis ajoutez-les au composant après l'avoir
Widget
enveloppé rendu des calques. Cependant, cette section sert principalement à illustrer comment l'utiliser dans le composant personnalisé Flutter , nous l'implémentons donc de manière personnalisée.RepaintBoundary
Stack
Layer
RenderObject
- Tout d'abord, nous définissons one
ChessWidget
, car ce n'est pas un composant de classe conteneur, il hérite donc deLeafRenderObjectWidget
:
class ChessWidget extends LeafRenderObjectWidget {
RenderObject createRenderObject(BuildContext context) {
return RenderChess(); // 返回Render对象
}
//...省略updateRenderObject函数实现
}
Étant donné que l'objet personnalisé RenderChess
n'accepte aucun paramètre, nous ChessWidget
n'avons pas besoin d'implémenter updateRenderObject
la méthode dans .
- Implémentation
RenderChess
; nous implémentons d'abord directement la version originale d'un échiquier non mis en cache, puis nous ajoutons un peu de code jusqu'à ce qu'il soit transformé en un objet pouvant mettre en cache l'échiquier.
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);//绘制棋子
}
}
- Ensuite, nous devons implémenter le cache de l'échiquier. Notre idée est la suivante :
- Créez un
Layer
échiquier spécialement dessiné, puis mettez-le en cache. - Lorsque le redessin est déclenché, si la zone de dessin a changé, redessiner l'échiquier et le mettre en cache ; si la zone de dessin n'a pas changé, utiliser directement la précédente
Layer
Pour ce faire, nous devons définir un PictureLayer
pour mettre en cache l'échiquier, puis ajouter une _checkIfChessboardNeedsUpdate
fonction pour implémenter la logique ci-dessus :
// 保存之前的棋盘大小
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);
}
Voir les commentaires pour la logique d'implémentation spécifique, et je n'entrerai pas dans les détails ici. Ce qu'il faut expliquer, c'est que dans paint
la méthode, chaque redessin doit être appelé context.addLayer(_layer)
pour ajouter l'échiquier layer
à l'arbre des couches actuel. Grâce à l'introduction dans la section précédente, nous savons que, en fait, est ajouté au premier nœud de bordure dessiné du nœud actuel Layer
. Certaines personnes peuvent se demander, si l'échiquier reste inchangé, il suffit de l'ajouter une fois, pourquoi faut-il l'ajouter à chaque fois que l'on redessine ? En fait, nous avons expliqué ce problème dans la section précédente, car le redessin est initié par le premier parent du nœud courant vers le bas, et avant chaque redessin, le nœud va d'abord effacer tous les enfants, voir la méthode pour le code , Nous avons donc PaintingContext.repaintCompositedChild
besoin pour l'ajouter à chaque fois que nous redessinons.
OK, maintenant que nous avons implémenté le cache de l'échiquier, vérifions-le.
Créons une démo de test pour vérifier, nous créons a ChessWidget
et a ElevatedButton
, car ElevatedButton
l'animation de la vague d'eau sera exécutée lorsqu'elle sera cliquée, donc une série de demandes de redessin sera initiée, et selon les connaissances de la section précédente, nous le savons ChessWidget
et le ferons être dessiné ElevatedButton
sur le même un redessin de entraînera également un redessin de . De plus, nous avons ajouté des journaux lors du dessin de pièces d'échecs et d'échiquiers, il nous suffit donc de cliquer sur , puis de vérifier les journaux pour vérifier si le cache de l'échiquier est en vigueur.Layer
ElevatedButton
ChessWidget
ElevatedButton
Remarque : dans la version actuelle (3.0) de Flutter, il
ElevatedButton
n'est pas ajouté dans l'implémentationRepaintBoundary
, il sera donc renduChessWidget
de la même manière queLayer
. S'il estElevatedButton
ajouté dans le SDK Flutter suivantRepaintBoundary
, il ne peut pas être vérifié par cet exemple.
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"),
),
],
),
);
}
}
Après avoir cliqué sur le bouton, on constate que l'échiquier et les pièces d'échecs peuvent être affichés normalement, comme indiqué sur la figure :
En même temps, le panneau de journal affiche beaucoup de " paint pieces
", et il n'y a pas de " paint chessboard
", ce qui indique que le cache de l'échiquier a pris effet.
好的,貌似我们预期的功能已经实现了,但是别高兴太早,上面的代码还有一个内存泄露的坑,我们在下面 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>();
On peut constater que nous RenderObject
en avons déjà défini un dans _layerHandle
, et il le gérera layer
; en même temps , layer
c'est un setter
, et attribuera automatiquement une nouvelle layer
valeur à _layerHandle
, nous pouvons donc RenderChess
directement utiliser la définition de la classe parente dans _layerHandle
, afin que nous plus besoin de le personnaliser One layerHandle
is gone.
La réponse est : cela dépend isRepaintBoundary
si l'attribut du nœud actuel est true
(c'est-à-dire si le nœud actuel est un nœud de frontière de dessin), si c'est le cas, true
ce n'est pas possible, si ce n'est pas le cas true
, c'est possible. Comme mentionné dans la section précédente, Flutter flushPaint
rencontre des nœuds de frontière de dessin lors du redessin :
- Vérifiez d'abord
layer
s'il est vide, s'il n'est pas vide, il effacera d'abordlayer
le nœud enfant du , puis l'utiliseralayer
pour en créer unPaintingContext
et le passer àpaint
la méthode. - S'il
layer
est vide, un est crééOffsetLayer
pour lui.
Si nous voulons layer
enregistrer l'échiquier dans une layer
variable prédéfinie, nous devons d'abord en créer une ContainerLayer
, puis ajouter celle qui dessine l'échiquier PictureLayer
en tant que nœud enfant à celle nouvellement créée ContainerLayer
, puis l'affecter à layer
la variable. Ainsi:
- Si nous définissons
RenderChess
surisRepaintBoundary
,true
le framework flutter effaceralayer
les nœuds enfants à chaque fois qu'il sera redessiné. De cette façon, notre échiquierPicturelayer
sera supprimé, puis une exception sera déclenchée. - Si
RenderChess
est (la valeur par défaut), le framework flutter n'utilisera pas l'attribut lors du redessin , et il n'y a pasisRepaintBoundary
de problème dans ce cas.false
layer
Bien que , RenderChess
dans cet exemple , il soit possible d'utiliser directement le calque, je vous le déconseille pour deux raisons :isRepaintBoundary
false
RenderObject
Le champ dulayer
framework Flutter est spécialement conçu pour le processus de dessin, selon le principe de séparation des tâches, il ne faut pas le frotter. Même si nous pouvons réussir maintenant, si un jour le flux de dessin de Flutter change, par exemple en commençant à utiliser des champs de nœud de frontière sans dessinlayer
, notre code aura des problèmes.- Si on veut l'utiliser
Layer
, il faut aussi en créer un au préalableContainerLayer
, dans ce cas autant en créer un directementLayerHandle
, ce qui est plus pratique.
Considérons maintenant la dernière question. Dans l'exemple ci-dessus, après avoir cliqué sur le bouton, même si l'échiquier ne sera pas redessiné, les pièces d'échecs seront toujours redessinées. Ceci est déraisonnable. Nous espérons que la zone de l'échiquier ne sera pas dérangée par l'extérieur monde, uniquement le nouveau comportement de déplacement (cliquez sur la zone de l'échiquier), puis redessinez les pièces d'échecs. Je crois qu'après l'avoir vue, la solution est prête à sortir. Nous avons deux options :
RenderChess
isRepaintBoundary
Le retour detrue
; transforme le nœud actuel en une limite de dessin, de sorte que et le bouton sera dessiné surChessWidget
des , et ne s'affectera pas l'un l'autre.layer
- Lorsque vous utilisez
ChessWidget
, définissez-le avec unRepaintBoundary
composant, qui est similaire au principe de 1, sauf que cette méthode transforme leChessWidget
nœud parent (RepaintBoundary
) en une limite de dessin (au lieu de lui-même), qui en créera également une nouvellelayer
pour isoler le bouton dessiner .
Lequel choisir dépend de la situation. La deuxième option sera plus flexible, mais l'effet réel de la première option est souvent meilleur, car si le contrôle d'auto-dessin complexe que nous encapsulons n'est pas défini sur , il est difficile pour nous isRepaintBoundary
pour true
garantir que les utilisateurs ajouteront nos contrôles lorsqu'ils l'utiliseront RepaintBoundary
, il serait donc préférable de protéger ce type de détails des utilisateurs.
Composition
Présentons-le ci-dessous flushCompositingBits()
. Passons maintenant en revue le pipeline de rendu de Flutter :
void drawFrame(){
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame()
...//省略
}
Parmi eux, seul flushCompositingBits()
n'a pas été introduit, c'est parce que pour comprendre flushCompositingBits()
, il est nécessaire de comprendre ce qu'est Layer et le processus de construction de l'arborescence Layer. Pour mieux comprendre, regardons d'abord une démo.
CustomRotatedBox
Implémentons-en un CustomRotatedBox
. Sa fonction est de réduire ses éléments enfants (rotation de 90 degrés dans le sens des aiguilles d'une montre). Pour obtenir cet effet, nous pouvons directement utiliser la canvas
fonction de transformation. Voici le code principal :
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();
}
... //省略无关代码
}
Écrivons une démo pour le tester :
class CustomRotatedBoxTest extends StatelessWidget {
const CustomRotatedBoxTest({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: Text(
"A",
textScaleFactor: 5,
),
),
);
}
}
L'effet de course est comme indiqué sur la figure, A a été abattu avec succès :
Maintenant, ajoutons- CustomRotatedBox
en un RepaintBoundary
pour réessayer :
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: RepaintBoundary( // 添加一个 RepaintBoundary
child: Text(
"A",
textScaleFactor: 5,
),
),
),
);
}
Après l'exécution, comme indiqué sur la figure :
Hé, pourquoi A s'est-il encore relevé !
Analysons la raison : sur la base des connaissances de la section précédente, nous pouvons facilement dessiner la RepaintBoundary
structure de l'arborescence des couches avant et après l'ajout, comme le montre la figure :
Après avoir ajouté RepaintBoundary
, CustomRotatedBox
le hold in est toujours 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();
}
... //省略无关代码
}
Évidemment, CustomRotatedBox
la transformation de rotation dans canvas
correspond à PictureLayer1
, et Text("A")
le dessin de correspond PictureLayer2
à canvas
, ils sont différents Layer
. On peut constater que le père et le fils PictureLayer
sont "séparés", donc CustomRotatedBox
ça ne Text("A")
fonctionnera pas sur . Alors comment résoudre ce problème ?
Comme nous l'avons présenté dans la section précédente, il existe de nombreux composants de conteneur avec des effets de transformation. La classe de conteneur Layer avec transformation de rotation est TransformLayer
, nous pouvons donc CustomRotatedBox
dessiner les nœuds enfants avant :
- Créez un
TransformLayer
(notéTransformLayer1
) à ajouter à l'arborescence des couches, puis créez un nouveauPaintingContext
etTransformLayer1
liez. - Les nœuds enfants sont dessinés à travers ce nouveau fichier
PaintingContext
.
Une fois les opérations ci-dessus terminées, l'endroit où les nœuds descendants sont dessinés PictureLayer
sera TransformLayer
les nœuds enfants de , afin que nous puissions TransformLayer
transformer tous les nœuds enfants dans leur ensemble via . La figure ci-dessous est TransformLayer1
l'arborescence des couches avant et après l'ajout.
Il s'agit en fait d'un processus de recomposition de couches : créez une nouvelle couche ContainerLayer
, puis ContainerLayer
transmettez-la aux nœuds enfants, de sorte que les nœuds descendants Layer
doivent appartenir à ContainerLayer
, puis ContainerLayer
la transformation de celle-ci prendra effet sur tous les nœuds descendants.
La "composition de calques" a différentes significations dans différents contextes. Par exemple, lorsque skia est finalement rendu, il rend également les calques un par un. Ce processus peut également être considéré comme la synthèse des informations de dessin sur plusieurs calques dans les informations bitmap finales ; dans De plus, canvas a également le concept de calque (
canvas.save
la méthode génère un nouveau calque), et le processus correspondant de superposition de tous les résultats de dessin de calque peut également être appelé synthèse de calque.
Jetons un coup d'œil à l'implémentation de code spécifique. Étant donné que la combinaison de couches est un processus standard (la seule différence est celle qui est utilisée ContainerLayer
comme conteneur parent), PantingContext
une méthode est fournie pushLayer
pour effectuer le processus de combinaison. Jetons un coup d'œil à son code source d'implémentation :
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();
}
Ensuite, il suffit d'en créer un TransformLayer
et de spécifier la transformation de rotation dont nous avons besoin, puis de l'appeler directement 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);
}
Ensuite, nous devons paint
juger si le nœud enfant est un nœud de frontière de dessin dans la méthode, si c'est le cas, il doit être combiné layer
, sinon, il doit être layer
combiné :
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;
}
}
Afin de rendre le code plus clair, nous child
encapsulons la logique de dessin lorsqu'elle n'est pas vide pushTransform
dans une fonction :
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;
}
}
Modifiez ensuite paint
l'implémentation et appelez pushTransform
directement la méthode :
void paint(PaintingContext context, Offset offset) {
if (child != null) {
pushTransform(
context,
child!.isRepaintBoundary,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
N'est-ce pas beaucoup plus clair ? Maintenant, reprenons l'exemple. L'effet est le même qu'auparavant, et A a été abattu avec succès !
Il convient de noter qu'en fait, PaintingContext
a déjà empaqueté pushTransform
la méthode pour nous, nous pouvons l'utiliser directement :
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.pushTransform(
child!.isRepaintBoundary,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
En fait, les méthodes correspondantes ont été encapsulées PaintingContext
pour la combinaison de classes de conteneurs communes avec des fonctions de transformation . En même temps, des composants avec des fonctions de transformation correspondantes ont été réservés dans Flutter. Voici un tableau correspondant :Layer
Nom du calque | Méthodes correspondant à PaintingContext | Widget |
---|---|---|
ClipPathLayer | pushClipPath | ClipPath |
Couche d'opacité | pushOpacity | Opacité |
ClipRRectLayer | pushClipRRect | ClipRRect |
ClipRectLayer | pushClipRect | ClipRect |
TransformLayer | pushTransform | RotatedBox、Transformer |
Quand avez-vous besoin de synthétiser Layer ?
1. Le principe de synthétiser Layer
Grâce à l'exemple ci-dessus, nous savons CustomRotatedBox
que les nœuds enfants directs de CustomRotatedBox
doivent être synthétisés lors du dessin des nœuds frontières layer
. En fait, ce n'est qu'un cas particulier, et il existe d'autres situations qui nécessitent CustomRotatedBox
une synthèse … Existe-t-il un principe universel général Layer
lorsque la synthèse est requise ? Layer
La réponse est oui! Réfléchissons à quelle est la cause profonde du CustomRotatedBox
besoin de synthèse dans ? Layer
Si CustomRotatedBox
tous les nœuds descendants partagent le même PictureLayer
, mais qu'une fois qu'un nœud descendant en crée un nouveau PictureLayer
, le dessin sera séparé du précédent PictureLayer
, car le dessin sur différents nœuds PictureLayer
est isolé les uns des autres et ne peut pas s'affecter, donc afin de make La transformation est effective pour tous les nœuds descendants correspondant à PictureLayer
, alors nous devons ajouter tous les nœuds descendants au même ContainerLayer
, nous devons donc synthétiser en CustomRotatedBox
premier Layer
.
Pour résumer, un principe universel ressort : lorsque le nœud descendant ajoutera une nouvelle classe de dessin Layer à l'arborescence des couches, la couche doit être synthétisée dans le composant de classe de transformation du parent.
Vérifions-le :
Modifions maintenant l'exemple ci-dessus pour RepaintBoundary
ajouter un Center
composant parent :
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: Center( // 新添加
child: RepaintBoundary(
child: Text(
"A",
textScaleFactor: 5,
),
),
),
),
);
}
Parce que CustomRotatedBox
ce n'est que lorsque le nœud enfant direct est jugé dans child!.isRepaintBoundary
,true
qu'il sera layer
synthétisé, mais maintenant son nœud enfant direct est Center
, donc le jugement sera false
, et ne sera pas layer
synthétisé. Cependant, selon la conclusion que nous avons tirée ci-dessus, RepaintBoundary
en tant que CustomRotatedBox
nœud descendant de et ajoutera un nouveau à l'arborescence des couches , il layer
doit être layer
synthétisé. Dans ce cas, il devrait être synthétisé layer
mais il n'est pas réellement synthétisé, il est donc s'attendait à ce que "A" ne puisse pas être placé Il était à l'envers, et après l'avoir exécuté, il s'est avéré que l'effet était "A", et ce n'était vraiment pas vers le bas !
Il semble que nous CustomRotatedBox
devions encore continuer à modifier. Il n'est pas difficile de résoudre ce problème. Lorsque nous jugeons s'il faut effectuer une synthèse de couche, nous devons parcourir l'ensemble du sous-arbre pour voir s'il existe un nœud de frontière de dessin. Si oui, alors synthétisez, sinon, non. Pour cela, nous définissons une nouvelle méthode pour trouver s'il y a un nœud de frontière de dessin sur le sous-arbre needCompositing()
:
//子树中递归查找是否存在绘制边界
needCompositing() {
bool result = false;
_visit(RenderObject child) {
if (child.isRepaintBoundary) {
result = true;
return ;
} else {
//递归查找
child.visitChildren(_visit);
}
}
//遍历子节点
visitChildren(_visit);
return result;
}
Ensuite, vous devez modifier paint
l'implémentation :
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.pushTransform(
needCompositing(), //子树是否存在绘制边界节点
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
Maintenant, lançons à nouveau la démo, l'effet après l'exécution :
Il a été abattu à nouveau! Mais il y a encore des problèmes, continuons à regarder vers le bas.
2. toujours besoin de composition
Considérons cette situation : si CustomRotatedBox
aucun nœud de bordure n'est dessiné parmi les nœuds descendants de , mais qu'une nouvelle couche est ajoutée à l'arborescence des couches par les nœuds descendants. Dans ce cas, selon nos conclusions précédentes, CustomRotatedBox
la synthèse de couches est également requise, mais pas CustomRotatedBox. Le problème est connu, mais ce problème n'est pas facile à résoudre. La raison en est que CustomRotatedBox
lorsque nous parcourons les nœuds descendants dans , nous ne pouvons pas savoir si le nœud frontière non dessin a ajouté une nouvelle couche à l'arborescence des couches. Comment faire? Flutter résout ce problème par convention :
-
RenderObject
Une propriété de type booléen est définie dansalwaysNeedsCompositing
. -
Convention : Dans le composant personnalisé, si le composant
isRepaintBoundary
estfalse
, si un nouveau calque est ajouté à l'arborescence des calques lors du dessin, il doit êtrealwaysNeedsCompositing
défini surtrue
.
Les développeurs doivent suivre cette spécification lors de la personnalisation des composants. Selon cette spécification, CustomRotatedBox
la condition de jugement lorsque nous effectuons une recherche récursive dans le sous-arbre peut être changée en :
child.isRepaintBoundary || child.alwaysNeedsCompositing
Au final notre needCompositing
implémentation est la suivante :
//子树中递归查找是否存在绘制边界
needCompositing() {
bool result = false;
_visit(RenderObject child) {
// 修改判断条件改为
if (child.isRepaintBoundary || child.alwaysNeedsCompositing) {
result = true;
return ;
} else {
child.visitChildren(_visit);
}
}
visitChildren(_visit);
return result;
}
Remarque : Cela nécessite que le composant de nœud non dessiné définisse sa propre alwaysNeedsCompositing
valeur lors de l'ajout d'un calque à l'arborescence des calques ture
.
Opacity
Jetons un coup d'œil à l'implémentation des composants dans flutter .
3. Analyse d'opacité
Opacity
Il est possible de contrôler la transparence des sous-arbres. Cet effet canvas
est difficile à obtenir via , donc le flottement utilise directement OffsetLayer
la méthode de synthèse pour obtenir :
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. Optimisation
Notez que ci-dessus, nous avons CustomRotatedBox
démontré le principe de base des composants de classe de transformation, mais il y a encore quelques optimisations, telles que :
- Dans le composant de classe de transformation, parcourir le sous-arbre pour déterminer si la composition de couches est requise est la logique générale du composant de classe de transformation, et il n'est pas nécessaire de l'implémenter dans chaque composant.
- Il n'est pas nécessaire de parcourir la sous-arborescence à chaque rafraîchissement. Par exemple, vous pouvez parcourir la sous-arborescence une fois lors de l'initialisation, puis mettre le résultat en cache. S'il y a une modification ultérieure, vous pouvez parcourir à nouveau la mise à jour et utiliser directement le résultat mis en cache. en ce moment.
Flutter a également pris en compte ce problème, il existe donc flushCompositingBits
une méthode, que nous présenterons formellement ci-dessous.
flushCompositingBits
Chaque nœud ( RenderObject
milieu) a un _needsCompositing
champ, qui est utilisé pour mettre en cache si le nœud actuel a besoin de synthétiser des couches lors du dessin des nœuds enfants. flushCompositingBits
La fonction de est de retraverser l'arbre de nœuds et de mettre à jour _needsCompositing
la valeur de chaque nœud lorsque l'arbre de nœuds est initialisé et que les informations composites dans le sous-arbre changent. Il peut être trouvé :
La logique de traversée récursive des sous-arbres est extraite dans flushCompositingBits
, et ne nécessite pas que les composants soient implémentés séparément.
Il n'est pas nécessaire de parcourir le sous-arbre à chaque redessin, uniquement lorsqu'il est initialisé et modifié.
Il résout parfaitement les problèmes que nous avons soulevés précédemment, regardons l'implémentation spécifique :
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
La fonction de _updateCompositingBits
la méthode est de parcourir récursivement le sous-arbre pour déterminer si _needsCompositing
la valeur de chaque nœud :
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;
}
Une fois l'exécution terminée, le de chaque nœud _needsCompositing
est déterminé. Nous n'avons qu'à juger du courant needsCompositing
(un getter
, qui sera retourné directement _needsCompositing
) lors du dessin pour savoir si le sous-arbre a une couche d'épluchage. Dans ce cas, nous pouvons optimiser CustomRenderRotatedBox
la mise en œuvre de et la mise en œuvre finale est la suivante :
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);
}
}
N'est-ce pas beaucoup plus concis et clair !
La signification de flushCompositingBits
Maintenant, réfléchissons à flushCompositingBits
quelle est la cause profonde de l'introduction ? Si nous utilisons toujours la méthode de la couche synthétique pour appliquer l'effet de transformation au sous-arbre dans le conteneur de classe de transformation, c'est-à-dire ne plus utiliser pour canvas
transformer, alors flushCompositingBits
il n'y a pas besoin d'existe, pourquoi doit-il l'être flushCompositingBits
? La raison fondamentale est la suivante : si la synthèse de couches à taille unique est utilisée dans les composants de transformation, au moins une couche sera créée à chaque fois qu'un composant de transformation est rencontré. De cette façon, le nombre de couches sur l'arborescence finale des couches sera augmenter. Nous avons dit précédemment que l'effet de transformation appliqué au sous-arbre peut être Canvas
implémenté à la fois via la classe de conteneur Layer et qu'il est recommandé de l'utiliser Canvas
. En effet, chaque nouvelle couche aura une surcharge supplémentaire, nous ne devrions donc Canvas
obtenir l'effet de changement de sous-arbre via la composition de calque que lorsqu'il est impossible d'obtenir l'effet de changement de sous-arbre. En résumé, nous pouvons constater que la cause profonde de l'introduction flushCompositingBits
est en fait de réduire le nombre de couches.
De plus, flushCompositingBits
le processus d'exécution est uniquement pour le marquage, et il n'y a pas de synthèse de couche.La vraie synthèse est lors du dessin ( paint
dans la méthode du composant).
Résumer
-
La recomposition n'est possible que s'il existe un conteneur de classe de transformation dans l'arborescence des composants
layer
; s'il n'y a pas de composant de classe de transformation, ce n'est pas obligatoire. -
La composition est requise dans le composant de classe de transformation lorsque les nœuds descendants du conteneur de classe de transformation ajouteront
layer
de nouvelles classes de dessin à l'arborescence .layer
layer
-
La raison fondamentale de l'introduction
flushCompositingBits
est de réduirelayer
le nombre de .
Résumé du processus de dessin
Ce qui suit peut être un bref résumé du processus de dessin de Flutter :
Résumé du processus du pipeline de rendu Flutter
La figure suivante est un résumé du processus de pipeline de rendu Flutter :
Dans la figure 5-16, après Vsync
l'arrivée du signal Engine
, le rafraîchissement de l'animation est d'abord terminé, puis le traitement des tâches moyennes et micro est Engine
lancé au milieu , et enfin renvoyé au milieu, et le travail de base du pipeline de rendu départs, comprenant principalement ces cinq étapes.Dart VM
Framework
Build、Layout、Paint、Composition、Rasterize
- Dans
Build
l'étape, la mise à jour des données d'origine sera effectuée sur la base deWidget Tree
, pilotée parElement Tree
(essentiellementBuildOwner
) ;Render Tree
- Dans
Layout
l'étape, le calcul des données de mise en page clés telles que la taille ( ) et le décalage ( )Render Tree
seraPipelineOwner
effectué sous la conduite de ;Size
Offset
- En
Paint
phase,Render Tree
sera basé surPaintingContext
le parcours de chaque nœud, mise à jourFramework
enLayer Tree
; - Dans
Composition
l'étape, les données de rendu finalesEngine
seront synthétisées avec l'entréeFramework
générée par l'étape , et soumises à ;Layer Tree
Scene
pipeline
- Dans
Rasterize
l'étape, les données à rendreRasterizer
serontpipeline
extraites de celui-ci, finalement dessinées sur la cibleSurface
et affichées à l'utilisateur.
référence: