Notes Flutter | Principes de base Flutter (4) Processus de dessin

Mécanisme Vsync

Lors du processus d'analyse du rendu de la première image, on peut constater Render Treeque la logique de rendu ( handleDrawFramemé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 Buffersont les données de l'image actuelle et l'autre moitié sont les données de l'image précédente.

insérez la description de l'image ici
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), setStatesi elle n'est pas dans l'état de rendu, la méthode sera appelée. code afficher comme ci-dessous:Element TreeensureVisualUpdatescheduleFrameensureVisualUpdate

void ensureVisualUpdate() {
    
    
  switch (schedulerPhase) {
    
    
    case SchedulerPhase.idle:  
    case SchedulerPhase.postFrameCallbacks:
      scheduleFrame();  
      return;
    case SchedulerPhase.transientCallbacks:   
    case SchedulerPhase.midFrameMicrotasks: 
    case SchedulerPhase.persistentCallbacks:
      return;
  }
}

scheduleFramecode afficher comme ci-dessous:

void scheduleFrame() {
    
    
  if (_hasScheduledFrame || !framesEnabled) return;
  ensureFrameCallbacksRegistered();  
  window.scheduleFrame();  
  _hasScheduledFrame = true;
}

Le code de la méthode est le ensureFrameCallbacksRegisteredsuivant :

// 代码清单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.onBeginFrameet window.onDrawFramesera 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 handleBeginFrameméthode et handleDrawFramemé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线程,然后调用RuntimeControllerScheduleFrame方法,最终会调用AnimatorRequestFrame方法,如代码清单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, callbackc'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, nullaffectez 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 Vsyncsignal d'enregistrement. Les deux rappels ci-dessus seront Vsynctraités ensemble lorsque le signal suivant arrivera.

Une fois la logique ci-dessus terminée, AwaitVSyncla méthode sera appelée pour enregistrer officiellement Vsyncle 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 Platformle 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.Vsyncjava_batonVsync

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.FlutterJNIasyncWaitForVsyncAsyncWaitForVsyncDelegateasyncWaitForVsync

// 代码清单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 Vsyncsignal suivant arrivera doFrame, où l'heure d'arrivée du signal frameTimeNanosindiqué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.VsyncVsyncdoFramefps6016.6msFlutterJNINativeVsyncVsync

Phase de réponse Vsync

Commençons par analyser nativeOnVsyncla 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_timela somme target_time, dont la signification a été expliquée précédemment. Le deuxième appel ConsumePendingCallbackrestaurera java_batonl'instance enregistrée et l'appellera CallbackLa 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 BeginFramemé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é vaut true.

Commençons à analyser la logique de rendu. OnAnimatorBeginFrameLa méthode sera appelée via la méthode Shell、Engine、Runtime-Controllerfinale , comme indiqué dans le Listing 5-35.PlatformConfigurationBeginFrame

// 代码清单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.onBeginFrameet window.onDrawFramela méthode, et appellera FlushMicrotasksNowla 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.onBeginFramedes méthodes et des méthodes. La logique du premier est montrée dans le Listing 5-36.window.onDrawFramehandleBeginFramehandleDrawFrame

// 代码清单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 _transientCallbacksdu rappel enregistré dans le champ, qui est généralement enregistré par animation, donc le premier paramètre de Timeline est ' Animate', puis _schedulerPhasele champ est marqué comme midFrameMicrotasks. On peut voir à partir de la liste de codes 5-35 qu'après handleBeginFramel'exécution de la méthode, la micro-tâche (Micro Task) sera traitée en premier, puis handleDrawFramel'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 _persistentCallbacksle champ et _postFrameCallbacksle 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é. drawFrameLa méthode est _persistentCallbacksla logique principale du champ. En raison de la relation d'héritage, WidgetsBindingla 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 buildScopela 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.drawFrameLa 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. finalizeTreeIl 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, flushLayoutla 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 ; flushPaintla méthode est chargée de traverser l'arborescence de rendu, d'exécuter Paintla logique de chaque nœud et de générer un Arborescence des couches. compositeFrameLa méthode est responsable de la construction Scenede 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.

insérez la description de l'image ici

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 Animatorest initialisé dans le constructeur et Producela 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 Continuationsignifie 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 Producela 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 ProducerCommitla 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é, Rasterizerla Drawmé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 Rastereffectué 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, Continuationl'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 Continuationpar le rendu de chaque image. trace_idBien que producer_continuation_les points d'appel soient très dispersés, du point de vue de la conception, Continuationle découplage des fonctions est garanti. Comme le montre la figure 5-10, après l'arrivée du signal Vsync, Animatorune Continuationinstance 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 Continuationla soumettent au layer_tree_pipeline_champ via l'objet, Rasterizerpuis la lisent pendant le processus réel. Le rendu et le niveau sont très bons.
insérez la description de l'image ici

Dans la Figure 5-10, pipelinele 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, Layeret 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 , Canvasle produit de dessin correspondant est enregistré dans PictureLayer.picturel'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 avec Scene.

Processus de dessin Flutter :

  1. Construisez-en un Canvaspour 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, et Canvaspeut initier en continu plusieurs instructions de dessin, et l'enregistreur d'instructions est utilisé pour collecter Canvastoutes les instructions de dessin dans un période de temps , donc Canvasle constructeur doit passer une instance comme premier paramètre PictureRecorder.

  2. CanvasUne fois le dessin terminé, PictureRecorderfaites passer le produit de dessin et enregistrez-le au format Layer.

  3. Construisez Scenel'objet layeret associez le dessin produit de Sceneavec .

  4. Sur l'écran, appelez window.renderl'API pour Sceneenvoyer 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é CustomPaintou 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 mainappeler directement ces API de bas niveau dans la fonction pour terminer. Montrons comment maindessiner 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 :
insérez la description de l'image ici

Image

PictureLayerLe produit de dessin dont nous avons parlé plus haut est Picturequ'il y a deux points à clarifier Picture:

PictureEn fait, il s'agit d'une série d'instructions d'opération de dessin graphique, qui peuvent faire référence aux Picturecommentaires du code source de la classe.
PicturePour ê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 Picturedessin 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 PictureLayerque 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 Pictureenregistre le produit de dessin, il devrait également fournir une méthode pour exporter le produit de dessin. En fait, Pictureil existe une toImagemé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 : Layerquel est le rôle du détenteur du produit de dessin ? La réponse est:

  1. frameLes artefacts de dessin peuvent être réutilisés entre différents (si aucun changement ne se produit).
  2. Divisez le contour du dessin et réduisez la plage de redessin .

LayerLes classes clés et leurs relations d'héritage sont les suivantes :

insérez la description de l'image ici

Dans la Figure 5-14, Layeril s'agit Layer Treede 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'autres Layernœuds, comme OpacityLayerl'ajout d'un effet de transparence aux nœuds enfants et ClipRectLayerla coupe des nœuds enfants. Nous appelons ContainerLayerla 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 PictureLayerla couche qui enregistre le produit de dessin, qui Layerest le nœud responsable de l'exécution du dessin réel. Ce nœud _picturecontient un ui.PictureRecorderobjet à 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 TextureLayeret PlatformViewLayer, leurs sources de rendu seront fournies en externe et Layerincorporées dans le rendu de trame de Flutter.

De plus, PaintingContextc'est Layerle contexte du dessin, fournissant Canvasl'objet pour le dessin final. ui.EngineLayerC'est Layerla représentation dans le moteur du framework Flutter, et sa structure Layerest 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 ?

  1. 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 RenderObjectstructure 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).

  2. 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 ContainerLayerne 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.ContainerLayerOffsetLayer

Classe de dessin Calque

Concentrons-nous sur PictureLayerla 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 Canvasdessinées par l'API. En fait, Canvasle produit de dessin de est Picturela représentation de l'objet, et ce n'est que dans la version actuelle de Flutter qu'il PictureLayera l'objet.En d'autres termes, le résultat du dessin du composant qui se dessine lui-même et ses nœuds enfants picturedans Flutter finira par tomber dans le .CanvasPictureLayer

Le choix de la méthode de réalisation de l'effet de transformation

Comme mentionné ci-dessus , certaines transformations ContainerLayerpeuvent être effectuées sur ses layersous-ensembles. En fait, la plupart Canvasdes 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 ContainerLayerréalisés de bout en Canvasbout. Par exemple, pour implémenter la transformation de la traduction, nous pouvons utiliser OffsetLayerou utiliser directement Canva.translatel'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. SkiaLa transformation de la classe de conteneur Layer est implémentée via la couche inférieure et n'a pas besoin Canvasd'être traitée. Le principe spécifique est que la classe conteneur Layer avec fonction de transformation correspondra à une Skiadans le moteur Layer.Afin de la distinguer de la Layer dans le framework Flutter, la dans Flutter Skiaest Layerappelée engine layer. Et la classe de conteneur Layer avec fonction de transformation Sceneen construira une avant de l'ajouter à engine layer, prenons comme OffsetLayerexemple 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();
}

OffsetLayerLa fonction de transformation de décalage pour ses nœuds enfants dans son ensemble est Skiaprise en charge par l'implémentation dans . SkiaIl peut prendre en charge le rendu multicouche, mais cela ne signifie pas que plus il y a de couches, mieux c'est, engineLayercela 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 Canvaspour y parvenir Canvas. très difficile ou impossible à réaliser sera ContainerLayerimplémenté en utilisant .

Donc, dans quels scénarios Canvasserait-il très difficile d'obtenir l'effet de transformation et doit-il être ContainerLayeratteint ? 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 Canvascorrespond généralement à un PictureLayer, et différents Canvass sont isolés les uns des autres.Ce n'est que Canvaslorsque tous les composants du sous-arbre sont dessinés à travers le même que la Canvastransformation globale de tous les nœuds enfants peut être effectuée via le , sinon il ne peut être passé que ContainerLayer.

Remarque : CanvasIl existe également ...layerune API associée nommée dans l'objet, par exemple Canvas.saveLayer, elle a une signification différente de celle présentée dans cette section Layer. CanvasL'objet layerest 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 tout Canvas layersimplement pensez que peu importe Canvascombien de paires sont créées layer, ces objets layersont tous sur le même objet PictureLayer.

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 :

insérez la description de l'image ici

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 RenderObjectet RenderObjectles principaux attributs liés au dessin sont :

  • layer
  • isRepaintBoundary( boolgenre)
  • needsCompositing( boolgenre)

dessiner des nœuds de bordure

Nous nous référons aux nœuds avec isRepaintBoundaryune valeur d'attribut en tant que nœuds de bordure de dessin.trueRenderObject

Flutter est livré avec un RepaintBoundarycomposant, 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 RenderObjectobjet a un needsCompositingattribut, qui est utilisé pour juger si lui et ses nœuds enfants ont une couche à synthétiser (si c'est le cas, cela truesignifie qu'il a une couche indépendante), et il y a aussi un _needsCompositingBitsUpdatechamp pour Flags si cette propriété doit être mise à jour. Avant le début de Paint, Flutter terminera d'abord needsCompositingla 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 layersera ContainerLayercréé OffsetLayeret attribué Donnez-le ; s'il n'est pas vide, utilisez-le directement. Ensuite, le nœud frontière est layerpassé aux nœuds enfants, puis il y a deux cas :

  1. Si le nœud enfant est un nœud non-frontière et doit être dessiné, lors du premier dessin :
    1) Créez un Canvasobjet et un PictureLayer, puis liez-les, et les appels ultérieurs Canvasà draw tomberont PictureLayersur la liaison.
    2) PictureLayerAjoutez ensuite ceci au nœud de frontière layer.
  2. Si ce n'est pas la première fois que vous dessinez, réutilisez les objets PictureLayeret existants Canvas.
  3. 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 layerajoutés au parent Layer.

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.

insérez la description de l'image ici
Jetons un coup d'œil au processus de génération :

  1. RenderViewC'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 le OffsetLayerpassons OffsetLayer1à OffsetLayer1le Row.
  2. 由于 Row 是一个容器类组件且不需要绘制自身,那么接下来他会绘制自己的孩子,它有两个孩子,先绘制第一个孩子Column1,将 OffsetLayer1 传给 Column1,而 Column1 也不需要绘制自身,那么它又会将 OffsetLayer1 传递给第一个子节点Text1
  3. Text1 需要绘制文本,他会使用 OffsetLayer1进行绘制,由于 OffsetLayer1 是第一次绘制,所以会新建一个PictureLayer1和一个 Canvas1 ,然后将 Canvas1PictureLayer1 绑定,接下来文本内容通过 Canvas1 对象绘制,Text1 绘制完成后,Column1 又会将 OffsetLayer1 传给 Text2
  4. Text2 也需要使用 OffsetLayer1 绘制文本,但是此时 OffsetLayer1 已经不是第一次绘制,所以会复用之前的 Canvas1PictureLayer1,调用 Canvas1来绘制文本。
  5. Column1 的子节点绘制完成后,PictureLayer1 上承载的是Text1Text2 的绘制产物。
  6. Ensuite, après Rowavoir terminé Column1le dessin de , commencez à dessiner le deuxième nœud enfant RepaintBoundaryet passez-le à , puisqu'il s'agit d'un nœud de frontière de dessin et Rowque 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.OffsetLayer1RepaintBoundaryOffsetLayer2RepaintBoundaryOffsetLayer2Column2Column1Column2OffsetLayer2Text3Text4
  7. Lorsque RepaintBoundaryles nœuds enfants de sont dessinés, RepaintBoundaryle layer( OffsetLayer2) de est ajouté au parent Layer( 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 PictureLayer1vous et OffsetLayer2êtes frères, et qu'ils sont tous les deux OffsetLayer1vos enfants. Grâce à l'exemple ci-dessus, nous pouvons au moins trouver une chose : la même chose Layerpeut être partagée par plusieurs composants, tels que Text1et Text2shared PictureLayer1.

Attendez, s'il est partagé, cela causera-t-il un problème, par exemple Text1lorsque 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, Layerun trop grand nombre Skiaconsommera 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 ContainerLayeret dans l'arborescence des couches PictureLayersont 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.WidgetWidgetLayerLayer

Marquer la marque d'étapeBesoinsRepeindre

RenderObjectLa demande de rafraîchissement est markNeedsRepaintinitiée en appelant Avant d'introduire markNeedsRepaintce 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 Layerles composants qui partagent le même doivent être redessinés. Par exemple, dans l'exemple ci-dessus, Text1s'il y a un changement, alors nous devons Text1redessiner en plusText2 ; s'il Text3y a un changement, alors nous devons également redessinerText4 ; comment y parvenir ?

Car qu'est -ce qui est partagé par Text1et 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 :Text2OffsetLayer1OffsetLayer1OffsetLayer1RenderViewText1Text2OffsetLayer2Text3Text4markNeedsRepaint

  1. 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 PiplineOwnerliste _nodesNeedingPaint(enregistrez le nœud de contour de dessin qui doit être redessiné).
  2. Au cours du processus de recherche, il _needsPaintdé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 sur true, indiquant qu'il doit être redessiné.
  3. Demandez-en un nouveau frame, exécutez le processus de redessiner.

markNeedsRepaintLe 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 elsebranche, car le nœud racine dans la version actuelle est un RenderView, et la propriété du composant isRepaintBoundaryest true, donc s'il est appelé, renderView.markNeedsPaint()il ira à la branche pour isRepaintBoundary.true

Après avoir demandé une nouvelle trame, drawFramele 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
    ...
  }
}

drawFrameLes trois fonctions liées à la neutralisation et au dessin impliquent flushCompositingBits, flushPaintet compositeFrame, et le processus de redessin est dans flushPaint, alors concentrons-nous flushPaintd'abord sur le processus. À propos de flushCompositingBits, il implique Layerla synthèse dans l'arbre des composants, que nous présenterons plus tard.

Flush stage flushPeinture

flushPaintMé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 Paintl'étape doit commencer à partir du nœud avec la plus grande profondeur , car l'effet du nœud ancêtre Paintdoit ê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 . PaintingContextLa méthode de repaintCompositedChildappellera éventuellement _repaintCompositedChildla méthode.

Ici, nous devons rappeler une chose, nous stateStateavons 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 markNeedsRepaintune méthode, et la méthode recherchera à partir du nœud actuel jusqu'à ce qu'il trouve un nœud isRepaintBoundaryc'est-à-dire true, puis ce sera Le nœud est ajouté à nodesNeedingPaintla liste. Par conséquent, nodesNeedingPaintles nœuds dans isRepaintBoundarydoivent être true, en d'autres termes, les nœuds qui peuvent être ajoutés à nodesNeedingPaintla liste sont tous des frontières de dessin , alors comment fonctionne cette frontière, continuons à regarder PaintingContext._repaintCompositedChildl'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éé OffsetLayerpour lui.

Un objet (noté ) est alors offsetLayerconstruit à partir de ,PaintingContextchildContext

Deuxièmement, childContextil est passé par des paramètres, généralement null, donc un nouvel PaintingContextobjet 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 .canvasPictureLayerCanvasPictureLayerchildContextcanvasPictureLayer

_paintWithContextLa 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, stopRecordingIfNeededla méthode est appelée pour arrêter l'enregistrement du travail pour l' PaintingContextobjet 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 _paintWithContextsuivant :

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 _needsPaintle 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 RenderObjectimplé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 RenderViewd'exemple, il finira par appeler paintChildla 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, _compositeChilddémarrer le dessin du nœud courant en créant un nouveau calque, sinon, appeler la _paintWithContextméthode précédemment analysée pour commencer à exécuter Paintla logique basée sur le couche courante.

Parmi eux, _compositeChildla 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 repaintCompositedChildpour créer un nouveau calque, et synchronisera les informations du calque offset, c'est-à-dire là où le calque commence à dessiner. Enfin, appelez appendLayerla méthode pour ajouter le nouveau calque à l'arbre des calques actuel.

Il y a trois points à noter ici :

  1. 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é ( _needsPaintfor false), il réutilisera directement le nœud frontière layersans redessiner ! C'est le principe selon lequel les nœuds frontières peuvent être réutilisés à travers les trames.
  2. Comme le type du nœud limite layerest ContainerLayer, il est possible d'y ajouter des nœuds enfants.
  3. Notez que le nœud limite actuel layerest ajouté au nœud limite parent , et non au nœud parent.

_containerLayerLes méthodes des champs sont appendles 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 adoptChildla méthode, étant donné que la plupart des alwaysNeedsAddToSceneattributs des nœuds Layer sont false, la méthode sera appelée markNeedsAddToScenepour indiquer la construction que le nœud actuel doit joindre Scene. SceneC'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 layerseront finalement connectés pour former un arbre de couches.

Analyse de la méthode de peinture de RenderColoredBox et RenderOpacity

Afin d'approfondir Paintla compréhension du nœud, les deux méthodes RenderObjectde nœud typiques suivantes paintsont analysées, la première est _RenderColoredBoxla paintmé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 RenderOpacityméthode paintest 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?);
  }
}

RenderOpacityIl 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' PaintContextobjet courant lorsqu'il est opaque, et il sera dessiné pushOpacitydans une méthode uniquement lorsqu'il est semi-transparent.OpacityLayer

Pour les cas semi-transparents, la méthode qui sera finalement appelée PaintingContextest pushOpacityindiqué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 Layerajoutez-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 appendLayerla 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 PaintingContextobjet pour dessiner un nouveau calque, qui est transmis painterpar un RenderObjectnœud spécifique, et RenderOpacitysa paintermé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 endRecordingla méthode appellera éventuellement une méthode dans le moteur.

Ensuite, analysez OpacityLayerla 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 paintChildla logique de la méthode a été introduite plus tôt. RenderOpacityIl est uniquement chargé de fournir un calque avec un effet transparent, et il RenderParagraphdoit Canvasêtre dessiné selon getla 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 PictureLayereffectué dans , PictureRecorderqui est responsable de la sauvegarde de toutes les instructions de dessin.

Il convient de noter que la logique ci-dessus Canvas, PictureRecorderetc. sont toutes héritées de la classe, qui est la classe parente NativeFieldWrapperClass2fournie 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.NativeCanvasPictureRecorderNative

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 ?

insérez la description de l'image ici

Étant donné que Text5est dessiné une fois le dessin terminé, que s'est-il passé après que le ( ) de a été ajouté au parent ( ) RepaintBoundarydans l'exemple ci-dessus lorsque les RepaintBoundarynœ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 :RepaintBoundarylayerOffsetLayer2LayerOffsetLayer1repaintCompositedChild

...
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 对应的 LayerOffsetLayer1,而 _currentLayerPictureLayer1_canvas 对应的是 Canvas1。我们看到实现很简单,先将 Canvas1 的绘制产物保存在 PictureLayer1 中,然后将一些变量都置空。

接下来再绘制 Text5 时,要先通过context.canvas 来绘制,根据 canvas getter的实现源码,此时就会走到 _startRecording() 方法,该方法我们上面介绍过,它会重新生成一个 PictureLayer 和一个新的 Canvas :

Canvas get canvas {
    
    
 //如果canvas为空,则是第一次获取;
 if (_canvas == null) _startRecording(); 
 return _canvas!;
}

之后,我们将新生成的 PictureLayerCanvas 记为 PictureLayer3Canvas3Text5 的绘制会落在 PictureLayer3 上,所以最终的 Layer Tree 如图:

insérez la description de l'image ici

我们总结一下:父节点在绘制子节点时,如果子节点是绘制边界节点,则在绘制完子节点后会生成一个新的 PictureLayer,后续其他子节点会在新的 PictureLayer 上绘制。

原理我们搞清楚了,但是为什么要这么做呢?直接复用之前的 PictureLayer1 有问题吗?

  • 答案是:在当前的示例中是不会有问题,但是在层叠布局(如 Stack 组件)的场景中就会有问题,下面我们看一个例子,结构图如下:

insérez la description de l'image ici

Sur la gauche se trouve une Stackmise en page et sur la droite se trouve la structure correspondante de l'arborescence des calques ; nous savons que Stackla 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 Child3est réutilisé lors du dessin PictureLayer1, il sera recouvert Child3par Child2, ce qui ne répond évidemment pas aux attentes, mais si vous en créez un nouveau et que vous PictureLayerl'ajoutez à OffsetLayerla fin, vous pouvez obtenir le résultat correct.

Maintenant, réfléchissons davantage : si Child2le nœud parent de n'est pas RepaintBoundary, cela signifie-t-il que Child3and Child1peut partager la même chosePictureLayer ?

  • la réponse est négative ! Si Child2le 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 nous TransformLayermettons Il est passé à Child2, Child2et une fois le dessin terminé, nous devons TransformLayerl'ajouter à Layerl'arborescence (il ne sera pas affiché s'il n'est pas ajouté à Layerl'arborescence), puis l'arborescence des composants et la structure finale de l'arborescence des couches sont affichées dans la figure :

insérez la description de l'image ici

RepaintBoudaryOn peut constater que cette situation est essentiellement la même que celle utilisée ci-dessus Child3et 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 PaintingContextles 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) :

insérez la description de l'image ici

cadre composite

Une fois créé layer, il doit être affiché à l'écran et cette partie du travail est renderView.compositeFrameeffectuée par la méthode. En fait, sa logique de mise en œuvre est très simple : d'abord via layerla construction Scene, et enfin via window.renderl'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 Sceneprocessus 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 addToScenechaque 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)layerSkianativeOffsetLayerPictureLayeraddToScenewindow.renderwindow.renderRender

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 Widgetenveloppé 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.RepaintBoundaryStackLayerRenderObject

  1. Tout d'abord, nous définissons one ChessWidget, car ce n'est pas un composant de classe conteneur, il hérite donc de LeafRenderObjectWidget:
class ChessWidget extends LeafRenderObjectWidget {
    
    
  
  RenderObject createRenderObject(BuildContext context) {
    
     
    return RenderChess(); // 返回Render对象
  }
  //...省略updateRenderObject函数实现
}

Étant donné que l'objet personnalisé RenderChessn'accepte aucun paramètre, nous ChessWidgetn'avons pas besoin d'implémenter updateRenderObjectla méthode dans .

  1. ImplémentationRenderChess ; 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);//绘制棋子
  }
}
  1. 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édenteLayer

Pour ce faire, nous devons définir un PictureLayerpour mettre en cache l'échiquier, puis ajouter une _checkIfChessboardNeedsUpdatefonction 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 paintla 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.repaintCompositedChildbesoin 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 ChessWidgetet a ElevatedButton, car ElevatedButtonl'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 ChessWidgetet le ferons être dessiné ElevatedButtonsur 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.LayerElevatedButtonChessWidgetElevatedButton

Remarque : dans la version actuelle (3.0) de Flutter, il ElevatedButtonn'est pas ajouté dans l'implémentation RepaintBoundary, il sera donc rendu ChessWidgetde la même manière que Layer. S'il est ElevatedButtonajouté dans le SDK Flutter suivant RepaintBoundary, 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 :

Illustration 14-16

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 RenderObjecten avons déjà défini un dans _layerHandle, et il le gérera layer; en même temps , layerc'est un setter, et attribuera automatiquement une nouvelle layervaleur à _layerHandle, nous pouvons donc RenderChessdirectement utiliser la définition de la classe parente dans _layerHandle, afin que nous plus besoin de le personnaliser One layerHandleis gone.

La réponse est : cela dépend isRepaintBoundarysi 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, truece n'est pas possible, si ce n'est pas le cas true, c'est possible. Comme mentionné dans la section précédente, Flutter flushPaintrencontre des nœuds de frontière de dessin lors du redessin :

  1. Vérifiez d'abord layers'il est vide, s'il n'est pas vide, il effacera d'abord layerle nœud enfant du , puis l'utilisera layerpour en créer un PaintingContextet le passer à paintla méthode.
  2. S'il layerest vide, un est créé OffsetLayerpour lui.

Si nous voulons layerenregistrer l'échiquier dans une layervariable prédéfinie, nous devons d'abord en créer une ContainerLayer, puis ajouter celle qui dessine l'échiquier PictureLayeren tant que nœud enfant à celle nouvellement créée ContainerLayer, puis l'affecter à layerla variable. Ainsi:

  1. Si nous définissons RenderChesssur isRepaintBoundary, truele framework flutter effacera layerles nœuds enfants à chaque fois qu'il sera redessiné. De cette façon, notre échiquier Picturelayersera supprimé, puis une exception sera déclenchée.
  2. Si RenderChessest (la valeur par défaut), le framework flutter n'utilisera pas l'attribut lors du redessin , et il n'y a pas isRepaintBoundaryde problème dans ce cas.falselayer

Bien que , RenderChessdans cet exemple , il soit possible d'utiliser directement le calque, je vous le déconseille pour deux raisons :isRepaintBoundaryfalse

  1. RenderObjectLe champ du layerframework 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 dessin layer, notre code aura des problèmes.
  2. Si on veut l'utiliser Layer, il faut aussi en créer un au préalable ContainerLayer, dans ce cas autant en créer un directement LayerHandle, 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 :

  1. RenderChessisRepaintBoundaryLe retour de true; transforme le nœud actuel en une limite de dessin, de sorte que et le bouton sera dessiné sur ChessWidgetdes , et ne s'affectera pas l'un l'autre.layer
  2. Lorsque vous utilisez ChessWidget, définissez-le avec un RepaintBoundarycomposant, qui est similaire au principe de 1, sauf que cette méthode transforme le ChessWidgetnœud parent ( RepaintBoundary) en une limite de dessin (au lieu de lui-même), qui en créera également une nouvelle layerpour 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 isRepaintBoundarypour truegarantir 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 canvasfonction 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 :

insérez la description de l'image ici
Maintenant, ajoutons- CustomRotatedBoxen un RepaintBoundarypour 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 :

insérez la description de l'image ici

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 RepaintBoundarystructure de l'arborescence des couches avant et après l'ajout, comme le montre la figure :

insérez la description de l'image ici

Après avoir ajouté RepaintBoundary, CustomRotatedBoxle 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, CustomRotatedBoxla transformation de rotation dans canvascorrespond à 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 PictureLayersont "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 CustomRotatedBoxdessiner les nœuds enfants avant :

  1. Créez un TransformLayer(noté TransformLayer1) à ajouter à l'arborescence des couches, puis créez un nouveau PaintingContextet TransformLayer1liez.
  2. 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 PictureLayersera TransformLayerles nœuds enfants de , afin que nous puissions TransformLayertransformer tous les nœuds enfants dans leur ensemble via . La figure ci-dessous est TransformLayer1l'arborescence des couches avant et après l'ajout.

insérez la description de l'image ici

Il s'agit en fait d'un processus de recomposition de couches : créez une nouvelle couche ContainerLayer, puis ContainerLayertransmettez-la aux nœuds enfants, de sorte que les nœuds descendants Layerdoivent appartenir à ContainerLayer, puis ContainerLayerla 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.savela 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 ContainerLayercomme conteneur parent), PantingContextune méthode est fournie pushLayerpour 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 TransformLayeret 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 paintjuger 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 layercombiné :

 
 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 childencapsulons la logique de dessin lorsqu'elle n'est pas vide pushTransformdans 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 paintl'implémentation et appelez pushTransformdirectement 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, PaintingContexta déjà empaqueté pushTransformla 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 PaintingContextpour 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 CustomRotatedBoxque les nœuds enfants directs de CustomRotatedBoxdoivent ê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 CustomRotatedBoxune synthèse … Existe-t-il un principe universel général Layerlorsque la synthèse est requise ? LayerLa réponse est oui! Réfléchissons à quelle est la cause profonde du CustomRotatedBoxbesoin de synthèse dans ? LayerSi CustomRotatedBoxtous 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 PictureLayerest 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 CustomRotatedBoxpremier 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 RepaintBoundaryajouter un Centercomposant parent :


Widget build(BuildContext context) {
    
    
  return Center(
    child: CustomRotatedBox(
      child: Center( // 新添加
        child: RepaintBoundary(
          child: Text(
            "A",
            textScaleFactor: 5,
          ),
        ),
      ),
    ),
  );
}

Parce que CustomRotatedBoxce n'est que lorsque le nœud enfant direct est jugé dans child!.isRepaintBoundary,true qu'il sera layersynthétisé, mais maintenant son nœud enfant direct est Center, donc le jugement sera false, et ne sera pas layersynthétisé. Cependant, selon la conclusion que nous avons tirée ci-dessus, RepaintBoundaryen tant que CustomRotatedBoxnœud descendant de et ajoutera un nouveau à l'arborescence des couches , il layerdoit être layersynthétisé. Dans ce cas, il devrait être synthétisé layermais 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 !

insérez la description de l'image ici

Il semble que nous CustomRotatedBoxdevions 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 paintl'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 :

insérez la description de l'image ici

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 CustomRotatedBoxaucun 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, CustomRotatedBoxla 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 CustomRotatedBoxlorsque 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 :

  1. RenderObjectUne propriété de type booléen est définie dans alwaysNeedsCompositing.

  2. Convention : Dans le composant personnalisé, si le composant isRepaintBoundaryest false, si un nouveau calque est ajouté à l'arborescence des calques lors du dessin, il doit être alwaysNeedsCompositingdéfini sur true.

Les développeurs doivent suivre cette spécification lors de la personnalisation des composants. Selon cette spécification, CustomRotatedBoxla 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 needCompositingimplé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 alwaysNeedsCompositingvaleur lors de l'ajout d'un calque à l'arborescence des calques ture.

OpacityJetons un coup d'œil à l'implémentation des composants dans flutter .

3. Analyse d'opacité

OpacityIl est possible de contrôler la transparence des sous-arbres. Cet effet canvasest difficile à obtenir via , donc le flottement utilise directement OffsetLayerla 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 CustomRotatedBoxdémontré le principe de base des composants de classe de transformation, mais il y a encore quelques optimisations, telles que :

  1. 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.
  2. 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 flushCompositingBitsune méthode, que nous présenterons formellement ci-dessous.

flushCompositingBits

Chaque nœud ( RenderObjectmilieu) a un _needsCompositingchamp, 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. flushCompositingBitsLa fonction de est de retraverser l'arbre de nœuds et de mettre à jour _needsCompositingla 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();
}

RenderObjectLa fonction de _updateCompositingBitsla méthode est de parcourir récursivement le sous-arbre pour déterminer si _needsCompositingla 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 _needsCompositingest 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 CustomRenderRotatedBoxla 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 à flushCompositingBitsquelle 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 canvastransformer, alors flushCompositingBitsil 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 Canvasimplé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 Canvasobtenir 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 flushCompositingBitsest en fait de réduire le nombre de couches.

De plus, flushCompositingBitsle 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 ( paintdans la méthode du composant).

Résumer

  1. La recomposition n'est possible que s'il existe un conteneur de classe de transformation dans l'arborescence des composantslayer ; s'il n'y a pas de composant de classe de transformation, ce n'est pas obligatoire.

  2. La composition est requise dans le composant de classe de transformation lorsque les nœuds descendants du conteneur de classe de transformation ajouteront layerde nouvelles classes de dessin à l'arborescence .layerlayer

  3. La raison fondamentale de l'introduction flushCompositingBitsest de réduire layerle nombre de .

Résumé du processus de dessin

Ce qui suit peut être un bref résumé du processus de dessin de Flutter :

insérez la description de l'image ici

Résumé du processus du pipeline de rendu Flutter

La figure suivante est un résumé du processus de pipeline de rendu Flutter :

insérez la description de l'image ici

Dans la figure 5-16, après Vsyncl'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 Enginelancé au milieu , et enfin renvoyé au milieu, et le travail de base du pipeline de rendu départs, comprenant principalement ces cinq étapes.Dart VMFrameworkBuild、Layout、Paint、Composition、Rasterize

  • Dans Buildl'étape, la mise à jour des données d'origine sera effectuée sur la base de Widget Tree, pilotée par Element Tree(essentiellement BuildOwner) ;Render Tree
  • Dans Layoutl'étape, le calcul des données de mise en page clés telles que la taille ( ) et le décalage ( ) Render Treesera PipelineOwnereffectué sous la conduite de ;SizeOffset
  • En Paintphase, Render Treesera basé sur PaintingContextle parcours de chaque nœud, mise à jour Frameworken Layer Tree;
  • Dans Compositionl'étape, les données de rendu finales Engineseront synthétisées avec l'entrée Frameworkgénérée par l'étape , et soumises à ;Layer TreeScenepipeline
  • Dans Rasterizel'étape, les données à rendre Rasterizerseront pipelineextraites de celui-ci, finalement dessinées sur la cible Surfaceet affichées à l'utilisateur.

référence:

Je suppose que tu aimes

Origine blog.csdn.net/lyabc123456/article/details/130951747
conseillé
Classement