Flutter 笔记 | Flutter 核心原理(四)绘制流程

Vsync 机制

在分析首帧渲染的过程中,可以发现Render Tree的渲染逻辑(handleDrawFrame方法)是直接执行的,但是后续每一帧的渲染都是Framework的主动调用导致的吗?实际上并非如此,也不能如此。试想一下,如果由Framework层控制每一帧的渲染,那么可能某一帧还没渲染完成,屏幕就开始刷新了,因为屏幕是按照自己的固有频率刷新的,而不会考虑具体的软件逻辑。此时,可能用于渲染的Buffer中,一半是当前帧的数据,一半是上一帧的数据,这就是所谓的“撕裂”(Tearing),如图5-9所示。

在这里插入图片描述
为了避免撕裂,大部分UI框架都会引入Vsync机制Vsync是垂直同步(Vertical Synchronization)的简称,其基本的思路是同步帧的渲染和显示器的刷新率。下面开始分析Flutter的Vsync机制。

Vsync 准备阶段

当UI需要更新一帧(通常是由于动画、手势或者直接调用setState导致Element Tree中出现脏节点)时,会调用ensureVisualUpdate方法, 如果没有处于渲染状态,将调用scheduleFrame方法。ensureVisualUpdate代码如下:

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

scheduleFrame代码如下:

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

其中ensureFrameCallbacksRegistered方法代码如下:

// 代码清单5-25 flutter/packages/flutter/lib/src/scheduler/binding.dart

void ensureFrameCallbacksRegistered() {
    
    
  window.onBeginFrame ??= _handleBeginFrame; // 由代码清单5-35调用
  window.onDrawFrame ??= _handleDrawFrame;   // 由代码清单5-35调用
}
void _handleBeginFrame(Duration rawTimeStamp) {
    
    
  if (_warmUpFrame) {
    
     // 首帧仍在渲染,见代码清单5-21
    _rescheduleAfterWarmUpFrame = true;
    return;
  }
  handleBeginFrame(rawTimeStamp); // 见代码清单5-36
}
void _handleDrawFrame() {
    
    
  if (_rescheduleAfterWarmUpFrame) {
    
     // 首帧预渲染导致的调用
    _rescheduleAfterWarmUpFrame = false;
    addPostFrameCallback((Duration timeStamp) {
    
    
      _hasScheduledFrame = false;
      scheduleFrame(); // 见代码清单5-20
    });
    return;
  }
  handleDrawFrame(); // 见代码清单5-37
}

window.onBeginFramewindow.onDrawFrame将在注册的Vsync信号到达后调用,这部分内容后面将详细分析,其对应的接口分别调用了handleBeginFrame方法和handleDrawFrame 方法,后面将详细分析其逻辑。

需要注意的是,如果_warmUpFrame字段为true,即通过Vsync驱动的渲染开始时发现首帧渲染仍在进行,则将_rescheduleAfterWarmUpFrame标记为true,并在_handleDrawFrame 中注册一个回调后退出,该回调将在首帧渲染后请求再次渲染一帧, 而这一帧将是通过Vsync信号驱动的。

下面分析window.scheduleFrame接口的逻辑,其对应的是一个Engine方法,如代码清单5-26所示。

// 代码清单5-26 engine/lib/ui/window/platform_configuration.cc
void ScheduleFrame(Dart_NativeArguments args) {
    
    
  UIDartState::ThrowIfUIOperationsProhibited(); // 确保在UI线程中
  UIDartState::Current()->platform_configuration()->client()->ScheduleFrame(); // 见代码清单5-27 
} // RuntimeController是client的具体实现类

以上逻辑主要检查当前是否处于UI线程,然后调用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
}

以上逻辑中,callback是必须携带的参数,因为没有回调的注册没有意义。接着,会检查callback_字段是否已经被设置,如果有则说明其他逻辑已经注册过了,直接返回;如果为null,则赋值为当前参数。注意,这里会接着检查secondary_callback_是否有值,其一般由触摸事件触发,其赋值时会触发AwaitVSync,所以此时callback_只需要完成赋值即可,而无须重复注册Vsync信号。以上两个回调会在后面Vsync信号到达时一并处理。

完成以上逻辑后,将调用AwaitVSync方法开始正式注册Vsync信号,如代码清单5-30所示。

// 代码清单5-30 engine/shell/platform/android/vsync_waiter_android.cc
void VsyncWaiterAndroid::AwaitVSync() {
    
    
  auto* weak_this = new std::weak_ptr<VsyncWaiter>(shared_from_this());
  jlong java_baton = reinterpret_cast<jlong>(weak_this); // 将当前实例变成long类型的id
  task_runners_.GetPlatformTaskRunner()->PostTask([java_baton]() {
    
     // 切换线程
    JNIEnv* env = fml::jni::AttachCurrentThread(); // 确保JNIEnv已准备完毕
    env->CallStaticVoidMethod(g_vsync_waiter_class->obj(), // Embedder中的FlutterJNI实例
                       g_async_wait_for_vsync_method_,  // 对应的Embedder方法
                       java_baton);  // Vsync到达后通过该参数调用本对象
  });
}

以上逻辑将在Java侧调用(在Platform线程中),这是因为NDK中没有监听硬件信号Vsync的API,而Android SDK中有。注意,java_baton是当前对象的弱引用指针,将用于Vsync信号到达时触发对应的回调。

该逻辑将调用Java侧的FlutterJNI对象的asyncWaitForVsync方法,最终将调用AsyncWaitForVsyncDelegateasyncWaitForVsync方法。由代码清单4-3可知,该对象在启动时已经完成注册,具体逻辑如代码清单5-31所示。

// 代码清单5-31 engine/shell/platform/android/io/flutter/view/VsyncWaiter.java
private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate =
  new FlutterJNI.AsyncWaitForVsyncDelegate() {
    
    
    
    public void asyncWaitForVsync(long cookie) {
    
     // cookie即代码清单5-30中的java_baton
      Choreographer.getInstance().postFrameCallback( // 为下一个Vsync信号注册回调
        new Choreographer.FrameCallback() {
    
    
          
          public void doFrame(long frameTimeNanos) {
    
     // Vsync到达时触发
            float fps = windowManager.getDefaultDisplay().getRefreshRate(); // 设备帧率
            long refreshPeriodNanos = (long) (1000000000.0 / fps); // 渲染一帧的最大耗时
            FlutterJNI.nativeOnVsync( // 调用Engine的方法,见代码清单5-32
                frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
          }
        }
      );
    }
  };

以上逻辑主要是调用系统API,该API将在下一个Vsync信号到达时调用doFrame方法,其中,由frameTimeNanos表示的Vsync信号到达的时间会稍稍早于该回调发生的时间,因为从Vsync信号到达到doFrame被调用,中间必然有一些逻辑耗时。接着通过系统API获取当前设备的帧率,并计算绘制一帧所需要的时间,对fps60的设备而言,绘制一帧需要16.6ms。最后将通过FlutterJNI调用Native方法,开始进行Vsync的响应,其中,第1个参数是Vsync信号到达时间;第2个参数表示当前帧最晚完成绘制的时间,它将贯穿整个渲染流程;第3个参数表示Flutter Engine中响应该信号的对象的指针。

Vsync 响应阶段

下面开始分析nativeOnVsync方法对应的Engine中的逻辑,如代码清单5-32所示。

// 代码清单5-32 engine/shell/platform/android/vsync_waiter_android.cc
void VsyncWaiterAndroid::OnNativeVsync( ...... ) {
    
    
  TRACE_EVENT0("flutter", "VSYNC");
  auto frame_time = fml::TimePoint::FromEpochDelta( // 时间格式转换
      fml::TimeDelta::FromNanoseconds(frameTimeNanos));
  auto target_time = fml::TimePoint::FromEpochDelta(
      fml::TimeDelta::FromNanoseconds(frameTargetTimeNanos));
  ConsumePendingCallback(java_baton, frame_time, target_time);
}
void VsyncWaiterAndroid::ConsumePendingCallback( ...... ) {
    
    
  auto* weak_this = reinterpret_cast<std::weak_ptr<VsyncWaiter>*>(java_baton);
  auto shared_this = weak_this->lock(); // 获取代码清单5-30中发起监听的VsyncWaiter实例
  delete weak_this;
  if (shared_this) {
    
     // 触发回调,以上由具体平台实现,以下是VsyncWaiter通用逻辑
    shared_this->FireCallback(frame_start_time, frame_target_time);
  }
}

以上逻辑首先提取frame_timetarget_time,其含义前面内容已解释过。其次调用ConsumePendingCallback,将java_baton还原成进行注册的实例,并调用其Callback,具体逻辑如代码清单5-33所示。

// 代码清单5-33 engine/shell/common/vsync_waiter.cc
void VsyncWaiter::FireCallback( ...... ) {
    
    
  Callback callback;
  fml::closure secondary_callback;
  {
    
    
    std::scoped_lock lock(callback_mutex_);
    callback = std::move(callback_); // callback_的赋值逻辑位于代码清单5-29
    secondary_callback = std::move(secondary_callback_);
  }
  if (!callback && !secondary_callback) {
    
     return; } // 没有回调,返回
  if (callback) {
    
    
    auto flow_identifier = fml::tracing::TraceNonce();
    task_runners_.GetUITaskRunner()->PostTaskForTime([ ...... ]() {
    
    
          callback(frame_start_time, frame_target_time); // 触发:见代码清单5-28
        }, frame_start_time);
  }
  if (secondary_callback) {
    
    
    task_runners_.GetUITaskRunner()->PostTaskForTime(
           std::move(secondary_callback), frame_start_time);
  }
}

以上逻辑提取callback_secondary_callback_,如果都为null则说明没有任何响应逻辑;如果有回调则在UI线程依次调用。对callback_字段而言,其赋值在代码清单5-28中,其BeginFrame方法如代码清单5-34所示。

// 代码清单5-34 engine/shell/common/animator.cc
void Animator::BeginFrame( ...... ) {
    
    
  // SKIP 第1步,Trace & Timeline存储
  frame_scheduled_ = false; // 当前不处于等待Vsync信号的状态
  notify_idle_task_id_++; // idle(空闲状态)的计数id,每帧递增,作用见后面内容
  regenerate_layer_tree_ = false; // 默认不重新生产layer_tree
  pending_frame_semaphore_.Signal(); // 释放信号,允许接收新的Vsync信号请求,见代码清单5-27
  if (!producer_continuation_) {
    
     // 第2步,产生一个待渲染帧,详见5.2.5节
    producer_continuation_ = layer_tree_pipeline_->Produce(); // 见代码清单5-40
    if (!producer_continuation_) {
    
     // 当前待渲染帧的队列已满
      RequestFrame(); // 重新注册Vsync,在下一帧尝试加入队列,见代码清单5-27
      return;
    }
  } // 第3步,重要属性的存储
  last_frame_begin_time_ = fml::TimePoint::Now(); // 帧渲染实际开始时间
  last_vsync_start_time_ = vsync_start_time; // Vsync信号通知的开始时间
  last_frame_target_time_ = frame_target_time; // 当前帧完成渲染的最晚时间
  dart_frame_deadline_ = FxlToDartOrEarlier(frame_target_time); // Dart VM的当前时间戳
  {
    
     // 第4步,开始渲染
    delegate_.OnAnimatorBeginFrame(frame_target_time); // 见代码清单5-35
  }
  if (!frame_scheduled_) {
    
     // 第5步,在UI线程注册任务,用于通知Dart VM当前空闲,可以进行GC等操作
    task_runners_.GetUITaskRunner()->PostDelayedTask([ ...... ]() {
    
    
      if (!self) {
    
     return; } 
      if (notify_idle_task_id == self->notify_idle_task_id_ && // 没有正在渲染的帧
          !self->frame_scheduled_) {
    
     // 不等待Vsync信号(准备渲染) 
        self->delegate_.OnAnimatorNotifyIdle(Dart_TimelineGetMicros() + 100000);
      } // 以上判断的核心是保证当前确实处于空闲状态
    }, kNotifyIdleTaskWaitTime);
  }
}

以上逻辑相对来说比较复杂,主要分为5步。第1步和第3步主要是一些重要属性的存储,代码中已有说明。第2步涉及一个复杂的设计,将在后面单独分析。第4步将触发Framework的渲染逻辑,这部分内容会在后面详细分析。第5步将在当前帧的渲染完成之后,在UI线程注册一个任务,同样是用于通知Dart VM当前处于空闲状态,可以进行GC等操作。这是非常有必要的,因为一帧绘制完成后Framework层会有大量对象的创建与销毁。该任务发出后能够执行的条件是当前没有新的帧待渲染(即保证渲染的优先级始终高于GC),具体表现为代码中的两个条件。

  • notify_idle_task_id == self->notify_idle_task_id_:说明当前没有正在渲染的帧,否则后者会自增。

  • !self->frame_scheduled_:说明没有正在等待Vsync信号的帧,否则该属性为true

下面开始分析渲染的逻辑。OnAnimatorBeginFrame方法将经由Shell、Engine、Runtime-Controller最终调用PlatformConfigurationBeginFrame方法,如代码清单5-35所示。

// 代码清单5-35 engine/lib/ui/window/platform_configuration.cc
void PlatformConfiguration::BeginFrame(fml::TimePoint frameTime) {
    
     
// 完成渲染的最晚时间
  std::shared_ptr<tonic::DartState> dart_state = begin_frame_.dart_state().lock();
  if (!dart_state) {
    
     return; }
  tonic::DartState::Scope scope(dart_state);
  int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds();
  tonic::LogIfError( tonic::DartInvoke(begin_frame_.Get(), // 见代码清单5-25
        {
    
     Dart_NewInteger(microseconds),})
  );
  UIDartState::Current()->FlushMicrotasksNow(); // 处理微任务
  tonic::LogIfError(tonic::DartInvokeVoid(draw_frame_.Get())); // 见代码清单5-25
}

以上逻辑将调用Framework的window.onBeginFramewindow.onDrawFrame方法,并在中间调用FlushMicrotasksNow方法以处理Dart VM的微任务。由此可以推断,帧渲染的逻辑将主要由Framework执行,下面开始详细分析。

Framework 响应阶段

由代码清单5-25可知,window.onBeginFramewindow.onDrawFrame接口所绑定的Dart 函数分别是handleBeginFrame方法和handleDrawFrame方法。前者逻辑如代码清单5-36所示。

// 代码清单5-36 flutter/packages/flutter/lib/src/scheduler/binding.dart
void handleBeginFrame(Duration? rawTimeStamp) {
    
    
  Timeline.startSync('Frame', arguments: ......);
  // 与时间戳相关字段的更新
  try {
    
    
     Timeline.startSync('Animate', arguments: ......); // Timeline事件
    _schedulerPhase = SchedulerPhase.transientCallbacks; // 更新状态
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks; 
	// 见代码清单8-36
    _transientCallbacks = <int, _FrameCallbackEntry>{
    
    }; // 处理高优先级的一次性回调
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
    
    
      if (!_removedIds.contains(id))
        _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, 
            callbackEntry.debugStack);
    });
    _removedIds.clear();
  } finally {
    
     // 更新状态,代码清单5-35中触发微任务消费
    _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
  }
}

以上逻辑主要处理_transientCallbacks字段中注册的回调,一般由动画注册,所以Timeline的第一个参数为’Animate’,然后将_schedulerPhase字段标记为midFrameMicrotasks。由代码清单5-35可知,handleBeginFrame方法执行完后确实会先处理完微任务(Micro Task),再触发handleDrawFrame方法的执行,如代码清单5-37所示。

// 代码清单5-37 flutter/packages/flutter/lib/src/scheduler/binding.dart
void handleDrawFrame() {
    
    
  Timeline.finishSync(); // 结束 Animate 阶段的统计
  try {
    
    
    _schedulerPhase = SchedulerPhase.persistentCallbacks; // 开始处理永久性回调
    for (final FrameCallback callback in _persistentCallbacks) // 一般是3棵树的更新
      _invokeFrameCallback(callback, _currentFrameTimeStamp!);
    _schedulerPhase = SchedulerPhase.postFrameCallbacks; // 处理低优先级的一次性回调
    final List<FrameCallback> localPostFrameCallbacks = // 当前帧渲染完成后触发
       List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (final FrameCallback callback in localPostFrameCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp!);
  } finally {
    
    
    _schedulerPhase = SchedulerPhase.idle; // Framework的帧渲染工作完成,当前进入空闲状态
    Timeline.finishSync(); // 结束帧,需要注意的是Raster线程仍将继续帧渲染工作
    _currentFrameTimeStamp = null;
  }
}

以上逻辑将依次处理_persistentCallbacks字段和_postFrameCallbacks字段中注册的回调,前者在启动过程中由Framework注册,执行后不会清除;后者一般由用户注册,每帧执行完之后都会清除。drawFrame方法为_persistentCallbacks字段中的主要逻辑,由于继承关系,将首先执行WidgetsBinding的逻辑,如代码清单5-38所示。

// 代码清单5-38 flutter/packages/flutter/lib/src/widgets/binding.dart
 // WidgetsBinding
void drawFrame() {
    
    
  // SKIP 首帧耗时统计相关
  try {
    
     // 开始更新3棵树,
    if (renderViewElement != null) // Element Tree的根节点
      buildOwner!.buildScope(renderViewElement!); // 见代码清单5-46
    super.drawFrame(); // 见代码清单5-39
    buildOwner!.finalizeTree(); // 见代码清单5-52
  } finally {
    
     ...... }
  // SKIP 首帧耗时统计相关
}

以上逻辑将执行buildScope方法,其主要工作是更新Element Tree的脏节点,并同步更新Render Tree。super.drawFrame方法则会根据Render Tree的信息完成Layout、Paint等工作,具体逻辑如代码清单5-39所示。finalizeTree 则会在UI线程的帧渲染工作结束后执行,主要负责清理Element Tree的无用节点,相关逻辑将在后面详细分析。

// 代码清单5-39 flutter/packages/flutter/lib/src/rendering/binding.dart
 // RendererBinding
void drawFrame() {
    
    
  pipelineOwner.flushLayout(); // 见代码清单5-55
  pipelineOwner.flushCompositingBits(); // 见代码清单5-63
  pipelineOwner.flushPaint(); // 见代码清单5-67
  if (sendFramesToEngine) {
    
    
    renderView.compositeFrame(); // 见代码清单5-83
    pipelineOwner.flushSemantics();
    _firstFrameSent = true;
  }
}

以上逻辑中,flushLayout方法负责更新Render Tree中每个节点的大小(Size)和位置(Offset)信息;flushPaint方法负责遍历Render Tree,执行每个节点的Paint逻辑,并生成Layer Tree。compositeFrame方法负责从Layer Tree构建Scene对象,并将通过Engine完成帧数据的最终渲染逻辑。

以上就是Flutter的Vsync机制:通过Framework发出请求,Engine将请求注册到Embedder的API中,并在Vsync信号到达时通过Engine回调到Framework,期间将先从UI线程切换到Platform线程,再从Platform线程切换回UI线程。

在这里插入图片描述

Continuation 设计分析

在代码清单5-34中,有一个非常晦涩的逻辑没有分析,即producer_continuation_,因为它并不是能够简单地通过该方法的上下文可以理解的。从Vsync信号到达之后,一帧的数据经过Build、Layout、Paint等各个阶段,到真正开始渲染时,该对象都会一直存在,如果分散解读,很有可能因为忽略了这个对象而无法窥见渲染管道的全貌,因此本节将单独分析。

代码清单5-34中的layer_tree_pipeline_对象在Animator的构造函数中完成初始化,并在开始渲染前调用Produce方法,其逻辑如代码清单5-40所示。

// 代码清单5-40 engine/shell/common/pipeline.h
explicit Pipeline(uint32_t depth) // 该参数默认为2
  : depth_(depth), empty_(depth), available_(0), inflight_(0) {
    
    }
ProducerContinuation Produce() {
    
    
  if (!empty_.TryWait()) {
    
     // 尝试产生一个待渲染帧,非阻塞式等待
    return {
    
    }; // empty_ 初始值为2,每次生产一帧计数减1,故最多能生产2帧
  }
  ++inflight_; // 待渲染帧数量加1
  return ProducerContinuation{
    
     // 当前待渲染帧尚无数据,故在此绑定提交数据的函数
             std::bind(&Pipeline::ProducerCommit, // 见代码清单5-41
             this, std::placeholders::_1, std::placeholders::_2),  // 参数占位符
             GetNextPipelineTraceID()};
}

由于渲染管道涉及多个线程,因此通过信号量empty_控制待渲染帧的数量,以上命名中的Continuation表示当前渲染一帧的任务存在但尚未完成。因为Vsync信号到达之后就已经确定要渲染一帧了,所以这里立刻通过Produce过程完成这个标记,这样的好处是将Vsync信号和最终的帧渲染一一对应。由于信号量的存在,将不会存在一个Vsync信号导致多帧渲染的情况,因为生产者和消费者是一一对应的。

在完成Framework层的渲染逻辑后,一帧的数据至此才算完全准备好,此时可以告知代码清单5-34中产生的producer_continuation_对象了,准确来说是提交待渲染的数据。具体的提交逻辑是调用前面内容绑定的ProducerCommit函数,如代码清单5-41所示。

// 代码清单5-41 engine/shell/common/pipeline.h
bool ProducerCommit(ResourcePtr resource, size_t trace_id) {
    
     // 见代码清单5-97
  {
    
    
    std::scoped_lock lock(queue_mutex_);
    queue_.emplace_back(std::move(resource), trace_id); // 将当前数据加入待渲染队列
  }
  available_.Signal(); // 计数1,新增一帧可用于渲染的资源
  return true;
}

注意,以上逻辑只是将待渲染数据(resource)提交到layer_tree_pipeline_对象的待渲染帧队列中,这里为什么不直接渲染呢?一是当前还是UI线程,无法渲染;二是渲染前的准备工作尚未完成,因此通过队列暂存。这里available_字段的作用和empty_字段相似,都是控制待渲染帧的数量。

在渲染相关的工作完全准备好之后,RasterizerDraw方法将对队列中的待渲染帧的数据进行消费,如代码清单5-42所示。

// 代码清单5-42 engine/shell/common/pipeline.h
[[nodiscard]] PipelineConsumeResult Consume(const Consumer& consumer) {
    
      
  if (consumer == nullptr) {
    
     return PipelineConsumeResult::NoneAvailable; } // 没有消费者
  if (!available_.TryWait()) {
    
     // 没有可消费的帧数据
    return PipelineConsumeResult::NoneAvailable;
  }
  ResourcePtr resource;
  size_t trace_id = 0;
  size_t items_count = 0;
  {
    
     // 取出第1个资源,进行处理
    std::scoped_lock lock(queue_mutex_);
    std::tie(resource, trace_id) = std::move(queue_.front()); // 提取数据
    queue_.pop_front(); // 移除队列的第1个待渲染帧数据
    items_count = queue_.size();
  }
  {
    
    
    TRACE_EVENT0("flutter", "PipelineConsume");
    consumer(std::move(resource)); // 一般将执行Rasterizer的DoDraw方法,见代码清单5-99
  }
  empty_.Signal(); // 释放资源,计数加1,可以响应新的渲染请求,见代码清单5-40
  --inflight_; // 标记当前待渲染的帧数量减1
  return items_count > 0 ? PipelineConsumeResult::MoreAvailable // 仍有待渲染的帧
                         : PipelineConsumeResult::Done; // 剩余0帧待渲染,完成
}

由于渲染是在Raster线程中进行,因此这里的锁是十分有必要的,以上逻辑的核心是取出layer_tree_pipeline_对象的待渲染帧队列中的第一个数据,并通过consumer函数进行真正的消费。

总的来说,Continuation的存在解决了以下两个问题。

  • Vsync信号与待渲染帧一一对应的问题(信号量)。

  • UI线程数据生产与Raster线程数据消费的时序问题(锁)。

此外,Continuation还通过trace_id为每一帧的渲染提供了追踪能力。虽然producer_continuation_的调用点十分分散,但是从设计上来说,Continuation保证了功能的解耦。如图5-10所示,Vsync信号到达后,Animator产生一个Continuation实例,Framework和Engine在UI线程完成一帧数据的合成并通过Continuation对象提交给layer_tree_pipeline_字段,Rasterizer在真正渲染时再进行读取,流程和层次都十分清晰。
在这里插入图片描述

图5-10中,pipeline所扮演的角色其实就是Android中的BufferQueue,即连接渲染数据的生产者(Framework)和消费者(Rasterizer)。

Flutter 绘制原理

Flutter中和绘制相关的对象有三个,分别是CanvasLayerScene

  • Canvas:封装了 Flutter Skia 各种绘制指令,比如画线、画圆、画矩形等指令。
  • Layer:分为容器类和绘制类两种;暂时可以理解为是绘制产物的载体,比如调用 Canvas 的绘制 API 后,相应的绘制产物被保存在 PictureLayer.picture 对象中。
  • Scene:屏幕上将要要显示的元素。在上屏前,我们需要将Layer中保存的绘制产物关联到 Scene 上。

Flutter 绘制流程:

  1. 构建一个 Canvas,用于绘制;同时还需要创建一个绘制指令记录器,因为绘制指令最终是要传递给 Skia 的,而 Canvas 可能会连续发起多条绘制指令,指令记录器用于收集 Canvas 在一段时间内所有的绘制指令,因此 Canvas 构造函数第一个参数必须传递一个 PictureRecorder 实例。

  2. Canvas 绘制完成后,通过 PictureRecorder 获取绘制产物,然后将其保存在 Layer 中。

  3. 构建 Scene 对象,将 layer 的绘制产物和 Scene 关联起来。

  4. 上屏,调用window.render API 将Scene上的绘制产物发送给GPU。

下面我们通过一个实例来演示整个绘制流程:

还记得之前绘制棋盘的例子吗,之前无论是通过CustomPaint还是自定义RenderObject,都是在Flutter的Widget框架模型下进行的绘制,实际上,最终到底层Flutter都会按照上述的流程去完成绘制,既然如此,那么我们也可以直接在main函数中调用这些底层API来完成,下面我们演示一下直接在main函数中在屏幕中绘制棋盘。

void main() {
    
    
  //1.创建绘制记录器和Canvas
  PictureRecorder recorder = PictureRecorder();
  Canvas canvas = Canvas(recorder);
  //2.在指定位置区域绘制。
  var rect = Rect.fromLTWH(30, 200, 300,300 );
  drawChessboard(canvas,rect); //画棋盘
  drawPieces(canvas,rect);//画棋子
  //3.创建layer,将绘制的产物保存在layer中
  var pictureLayer = PictureLayer(rect);
  //recorder.endRecording()获取绘制产物。
  pictureLayer.picture = recorder.endRecording();
  var rootLayer = OffsetLayer();
  rootLayer.append(pictureLayer);
  //4.上屏,将绘制的内容显示在屏幕上。
  final SceneBuilder builder = SceneBuilder();
  final Scene scene = rootLayer.buildScene(builder);
  window.render(scene);
}

运行效果:
在这里插入图片描述

Picture

上面我们说过 PictureLayer 的绘制产物是 Picture,关于 Picture 有两点需要阐明:

Picture 实际上是一系列的图形绘制操作指令,这一点可以参考 Picture 类源码的注释。
Picture 要显示在屏幕上,必然会经过光栅化,随后Flutter会将光栅化后的位图信息缓存起来,也就是说同一个 Picture 对象,其绘制指令只会执行一次,执行完成后绘制的位图就会被缓存起来。

综合以上两点,我们可以看到 PictureLayer 的“绘制产物”一开始是一些列“绘图指令”,当第一次绘制完成后,位图信息就会被缓存,绘制指令也就不会再被执行了,所以这时“绘制产物”就是位图了。为了便于理解,后续我们可以认为指的就是绘制好的位图。

Canvas绘制的位图转图片

既然 Picture 中保存的是绘制产物,那么它也应该能提供一个方法能将绘制产物导出,实际上,Picture有一个toImage方法,可以根据指定的大小导出Image

//将图片导出为Uint8List
final Image image = await pictureLayer.picture.toImage();
final ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
final Uint8List pngBytes = byteData!.buffer.asUint8List();
print(pngBytes);

Layer

现在我们思考一个问题:Layer作为绘制产物的持有者有什么作用? 答案就是:

  1. 可以在不同的frame之间复用绘制产物(如果没有发生变化)。
  2. 划分绘制边界,缩小重绘范围

Layer关键类及其继承关系如下:

在这里插入图片描述

在图5-14中,LayerLayer Tree中所有节点的基类,其子类主要分为3种。

  • 第1种是ContainerLayer,顾名思义就是其他Layer节点的容器,比如OpacityLayer为子节点增加一个透明度的效果,ClipRectLayer对子节点进行裁剪。我们将直接继承自ContainerLayer 类的 Layer 称为容器类Layer,容器类 Layer 可以添加任意多个子Layer。
  • 第2种是PictureLayer,保存绘制产物的 Layer,该Layer是负责执行实际绘制的节点,该节点通过_picture字段持有一个ui.PictureRecorder对象,用于Engine进行对应绘制指令的记录。我们将可以直接承载(或关联)绘制结果的 Layer 称为绘制类 Layer
  • 第3种是TextureLayerPlatformViewLayer,它们的渲染源将由外部提供,并通过Layer纳入Flutter的帧渲染中。

除此之外,PaintingContextLayer进行绘制的上下文,提供进行最终绘制的Canvas对象。ui.EngineLayer则是Flutter Framework中的Layer在Engine中的表示,其结构和Framework中的Layer结构(即图5-14)几乎一致,在此不再赘述。

容器类 Layer 的作用

上面介绍的容器类 Layer 的概念,那么它的作用和具体使用场景是什么呢?

  1. 将组件树的绘制结构组成一棵树

    因为 Flutter 中的 Widget 是树状结构,那么相应的 RenderObject 对应的绘制结构也应该是树状结构,Flutter 会根据一些“特定的规则” 为组件树生成一棵 Layer 树,而容器类Layer就可以组成树状结构(父 Layer 可以包含任意多个子 Layer,子Layer又可以包含任意多个子Layer)。

  2. 可以对多个 layer 整体应用一些变换效果

    容器类 Layer 可以对其子 Layer 整体做一些变换效果,比如剪裁效果(ClipRectLayer、ClipRRectLayer、ClipPathLayer)、过滤效果(ColorFilterLayer、ImageFilterLayer)、矩阵变换(TransformLayer)、透明变换(OpacityLayer)等。

虽然 ContainerLayer 并非抽象类,开发者可以直接创建 ContainerLayer 类的示例,但实际上很少会这么做,相反,在需要使用使用 ContainerLayer 时直接使用其子类即可, 如果我们确实不需要任何变换效果,那么就使用 OffsetLayer,不用担心会有额外性能开销,它的底层(Skia 中)实现是非常高效的。

绘制类 Layer

下面我们重点介绍一下 PictureLayer 类,它是 Flutter 中最常用的一种绘制类Layer

我们知道最终显示在屏幕上的是位图信息,而位图信息正是由 Canvas API 绘制的。实际上,Canvas 的绘制产物是 Picture 对象表示,而当前版本的 Flutter 中只有 PictureLayer 才拥有 picture 对象,换句话说,Flutter 中通过Canvas 绘制自身及其子节点的组件的绘制结果最终会落在 PictureLayer 中。

变换效果实现方式的选择

上面说过 ContainerLayer 可以对其子 layer 整体进行一些变换,实际上,在大多数UI系统的 Canvas API 中也都有一些变换相关的 API ,那么也就意味着一些变换效果我们既可以通过 ContainerLayer 来实现,也可以通过 Canvas 来实现。比如,要实现平移变换,我们既可以使用 OffsetLayer ,也可以直接使用 Canva.translate API。既然如此,那我们选择实现方式的原则是什么呢?

现在,我们先了解一下容器类 Layer 实现变换效果的原理。容器类 Layer的变换在底层是通过 Skia 来实现的,不需要 Canvas 来处理。具体的原理是,有变换功能的容器类 Layer 会对应一个 Skia 引擎中的 Layer,为了和 Flutter framework 中 Layer 区分,flutter 中将 SkiaLayer 称为 engine layer。而有变换功能的容器类 Layer 在添加到 Scene 之前就会构建一个 engine layer,我们以 OffsetLayer 为例,看看其相关实现:


void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
    
    
  // 构建 engine layer
  engineLayer = builder.pushOffset(
    layerOffset.dx + offset.dx,
    layerOffset.dy + offset.dy,
    oldLayer: _engineLayer as ui.OffsetEngineLayer?,
  );
  addChildrenToScene(builder);
  builder.pop();
}

OffsetLayer 对其子节点整体做偏移变换的功能是 Skia 中实现支持的。Skia 可以支持多层渲染,但并不是层越多越好,engineLayer 是会占用一定的资源,Flutter 自带组件库中涉及到变换效果的都是优先使用 Canvas 来实现,如果 Canvas 实现起来非常困难或实现不了时才会用 ContainerLayer 来实现。

那么有什么场景下变换效果通过 Canvas 实现起来会非常困难,需要用 ContainerLayer 来实现 ?一个典型的场景是,我们需要对组件树中的某个子树整体做变换,且子树中有多个 PictureLayer 时。这是因为一个 Canvas 往往对应一个 PictureLayer,不同 Canvas 之间相互隔离的,只有子树中所有组件都通过同一个 Canvas 绘制时才能通过该 Canvas 对所有子节点进行整体变换,否则就只能通过 ContainerLayer

注意:Canvas对象中也有名为 ...layer 相关的 API,如 Canvas.saveLayer,它和本节介绍的Layer 含义不同。Canvas对象中的 layer 主要是提供一种在绘制过程中缓存中间绘制结果的手段,为了在绘制复杂对象时方便多个绘制元素之间分离绘制而设计的,更多关于 Canvas layer 相关API可以查阅相关文档,我们可以简单认为不管 Canvas 对创建多少个 layer,这些 layer 都是在同一个 PictureLayer 上。

示例

下面我们看一段代码,了解它所对应的 Layer 是什么样子的:

void main() {
    
    
  var imageUrl = " ...... ";
  var direct = TextDirection.ltr;
  runApp(Container(
    child: Row(textDirection: direct,
      children: [
        RepaintBoundary(
          child: Image.network(imageUrl, width: 100, excludeFromSemantics: true,)),
        Opacity(opacity: 0.5,
          child: Image.network(imageUrl, width: 100, excludeFromSemantics: true,))
      ],),));
  Timer.run(() {
    
     // 输出 Widget Tree/Render Tree/Layer Tree的信息
    debugDumpApp();
    debugDumpRenderTree();
    debugDumpLayerTree();
  });
}

以上代码所对应的 Render Tree 及 Layer Tree 如图所示:

在这里插入图片描述

所以 Flutter 中实际上存在着4棵树,即 Layer Tree,使用 Layer Tree 的好处是可以做Paint流程的局部更新(没错,Flutter中局部更新的思想无处不在),比如视频播放时其上面的“播放”按钮、进度条等控件没有必要每一帧都进行 Paint。此外,Flutter的列表正是借助 Layer Tree 实现高效滑动:Flutter 的列表中,每个 Item 拥有一个独立的 Layer,这样在滑动的时候只需要更新 Layer 的位置信息,而不需要重新绘制内容。

组件树绘制流程

绘制相关实现在渲染对象 RenderObject 中,RenderObject 中和绘制相关的主要属性有:

  • layer
  • isRepaintBoundarybool类型)
  • needsCompositing (bool类型)

绘制边界节点

我们将 isRepaintBoundary 属性值为 trueRenderObject 节点称为绘制边界节点。

Flutter 自带了一个 RepaintBoundary 组件,它的功能其实就是向组件树中插入一个绘制边界节点。

needsCompositing

Render Tree 中,每个RenderObject对象都拥有一个needsCompositing属性,用于判断自身及其子节点是否有一个要去合成的图层(若为true则说明自身拥有一个独立的图层),同时还有一个_needsCompositingBitsUpdate字段用于标记该属性是否需要更新。Flutter 在 Paint 开始前首先会完成needsCompositing属性的更新,然后开始正式绘制。

我们先讲一下Flutter绘制组件树的一般流程,注意,并非完整流程,因为我们暂时会忽略子树中需要“层合成”(Compositing)的情况,这部分我们会在后面讲到。下面是大致流程:

Flutter第一次绘制时,会从上到下开始递归的绘制子节点,每当遇到一个边界节点,则判断如果该边界节点的 layer 属性为空(类型为ContainerLayer),就会创建一个新的 OffsetLayer 并赋值给它;如果不为空,则直接使用它。然后会将边界节点的 layer 传递给子节点,接下来有两种情况:

  1. 如果子节点是非边界节点,且需要绘制,则会在第一次绘制时:
    1)创建一个Canvas 对象和一个 PictureLayer,然后将它们绑定,后续调用Canvas 绘制都会落到和其绑定的PictureLayer 上。
    2)接着将这个 PictureLayer 加入到边界节点的 layer 中。
  2. 如果不是第一次绘制,则复用已有的 PictureLayerCanvas 对象 。
  3. 如果子节点是边界节点,则对子节点递归上述过程。当子树的递归完成后,就要将子节点的layer 添加到父级 Layer中。

整个流程执行完后就生成了一棵 Layer Tree。下面我们通过一个例子来理解整个过程:下图左边是 widget 树,右边是最终生成的 Layer 树

在这里插入图片描述
我们看一下生成过程:

  1. RenderView 是 Flutter 应用的根节点,绘制会从它开始,因为他是一个绘制边界节点,在第一次绘制时,会为他创建一个 OffsetLayer,我们记为 OffsetLayer1,接下来 OffsetLayer1会传递给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. 接下来 Row 完成了 Column1 的绘制后,开始绘制第二个子节点 RepaintBoundaryRow 会将 OffsetLayer1 传递给 RepaintBoundary,由于它是一个绘制边界节点,且是第一次绘制,则会为它创建一个 OffsetLayer2,接下来 RepaintBoundary 会将 OffsetLayer2 传递给Column2,和 Column1 不同的是,Column2 会使用 OffsetLayer2 去绘制 Text3Text4,绘制过程同Column1,在此不再赘述。
  7. RepaintBoundary 的子节点绘制完时,要将 RepaintBoundarylayerOffsetLayer2 )添加到父级LayerOffsetLayer1)中。

至此,整棵组件树绘制完成,生成了一棵右图所示的 Layer 树。需要说名的是 PictureLayer1OffsetLayer2 是兄弟关系,它们都是 OffsetLayer1 的孩子。通过上面的例子我们至少可以发现一点:同一个 Layer 是可以多个组件共享的,比如 Text1Text2 共享 PictureLayer1

等等,如果共享的话,会不会导致一个问题,比如 Text1 文本发生变化需要重绘时,是不是也会连带着 Text2 也必须重绘?

答案是:是!这貌似有点不合理,既然如此那为什么要共享呢?不能每一个组件都绘制在一个单独的 Layer 上吗?这样还能避免相互干扰。原因其实还是为了节省资源,Layer 太多时 Skia 会比较耗资源,所以这其实是一个 trade-off。

再次强调一下,上面只是绘制的一般流程。一般情况下 Layer 树中的 ContainerLayerPictureLayer 的数量和结构是和 Widget 树中的边界节点一一对应的,注意并不是和 Widget一一对应。 当然,如果 Widget 树中有子组件在绘制过程中添加了新的 Layer,那么 Layer 会比边界节点数量多一些,这时就不是一一对应了。另外,Flutter 中很多拥有变换、剪裁、透明等效果的组件的实现中都会往 Layer 树中添加新的 Layer。

Mark 阶段 markNeedsRepaint

RenderObject 是通过调用 markNeedsRepaint 来发起重绘请求的,在介绍 markNeedsRepaint 具体做了什么之前,我们根据上面介绍的 Flutter绘制流程先猜一下它应该做些什么?

我们知道绘制过程存在Layer共享,所以重绘时,需要重绘所有共享同一个Layer的组件。比如上面的例子中,Text1发生了变化,那么我们除了 Text1 也要重绘 Text2;如果 Text3 发生了变化,那么也要重绘Text4;那如何实现呢?

因为Text1Text2 共享的是 OffsetLayer1,而 OffsetLayer1 的拥有者是谁呢?找到它让它重绘不就行了!OK,可以很容发现 OffsetLayer1 的拥有者是根节点 RenderView,它同时也是 Text1Text2的第一个父级绘制边界节点。同样的,OffsetLayer2 也正是 Text3Text4 的第一个父级绘制边界节点,所以我们可以得出一个结论:当一个节点需要重绘时,我们得找到离它最近的第一个父级绘制边界节点,然后让它重绘即可,而 markNeedsRepaint 正是完成了这个过程,当一个节点调用了它时,具体的步骤如下:

  1. 会从当前节点一直往父级查找,直到找到一个绘制边界节点时终止查找,然后会将该绘制边界节点添加到其PiplineOwner_nodesNeedingPaint列表中(保存需要重绘的绘制边界节点)。
  2. 在查找的过程中,会将自己到绘制边界节点路径上所有节点的_needsPaint属性置为true,表示需要重新绘制。
  3. 请求新的 frame ,执行重绘重绘流程。

markNeedsRepaint 删减后的核心源码如下:

void markNeedsPaint() {
    
    
  if (_needsPaint) return;
  _needsPaint = true;
  if (isRepaintBoundary) {
    
     // 如果是当前节点是边界节点
      owner!._nodesNeedingPaint.add(this); //将当前节点添加到需要重新绘制的列表中。
      owner!.requestVisualUpdate(); // 请求新的frame,该方法最终会调用scheduleFrame()
  } else if (parent is RenderObject) {
    
     // 若不是边界节点且存在父节点
    final RenderObject parent = this.parent! as RenderObject;
    parent.markNeedsPaint(); // 递归调用父节点的markNeedsPaint
  } else {
    
     // 非RenderObject节点
    // 一般不会发生,即当前节点不是一个`RenderObject`节点,此时直接请求帧渲染。
    if (owner != null)
      owner!.requestVisualUpdate();
  }
}

值得一提的是,在当前版本的Flutter中是永远不会走到最后一个else分支的,因为当前版本中根节点是一个RenderView,而该组件的isRepaintBoundary 属性为 true,所以如果调用 renderView.markNeedsPaint()是会走到isRepaintBoundarytrue的分支的。

请求新的 frame 后,下一个 frame 到来时就会走drawFrame流程,回忆一下该方法:

void drawFrame() {
    
    
  buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
  //下面是 展开 super.drawFrame() 方法
  pipelineOwner.flushLayout(); // 2.更新布局
  pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
  pipelineOwner.flushPaint(); // 4.重绘
  if (sendFramesToEngine) {
    
    
    renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
    ...
  }
}

drawFrame中和绘制相关的涉及flushCompositingBitsflushPaintcompositeFrame 三个函数,而重新绘制的流程在 flushPaint 中,所以我们先重点看一下flushPaint的流程,关于 flushCompositingBits ,它涉及组件树中Layer的合成,我们会在后面介绍 。

Flush 阶段 flushPaint

flushPaint 方法如下:

void flushPaint() {
    
     
  if (!kReleaseMode) {
    
     Timeline.startSync('Paint', arguments: ......); } 
  // 开始绘制
  try {
    
    
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
    _nodesNeedingPaint = <RenderObject>[];
    for (final RenderObject node in dirtyNodes..sort( // 排序,优先绘制子节点
      (RenderObject a, RenderObject b) => b.depth - a.depth)) {
    
    
      if (node._needsPaint && node.owner == this) {
    
    
        if (node._layer!.attached) {
    
    
          PaintingContext.repaintCompositedChild(node); //  
        } else {
    
    
          node._skippedPaintingOnLayer();
        }
      } // if
    } // for
  } finally {
    
    
    if (!kReleaseMode) {
    
     Timeline.finishSync(); } // 结束绘制
  }
}

以上逻辑会按照深度顺序,从最深的节点开始依次调用repaintCompositedChild方法。需要注意的是,前几个阶段都是从深度最小的节点开始处理,但是Paint阶段要从深度最大的节点开始,因为祖先的节点的Paint效果必须作用于子节点,比如一个裁剪节点,要对子节点产生裁剪效果,必须等子节点完成绘制才行PaintingContextrepaintCompositedChild方法最终会调用_repaintCompositedChild方法。

这里需要提醒一点,我们在介绍stateState流程一节说过,组件树中某个节点要更新自己时会调用markNeedsRepaint方法,而该方法会从当前节点一直往上查找,直到找到一个isRepaintBoundarytrue 的节点,然后会将该节点添加到 nodesNeedingPaint列表中。因此,nodesNeedingPaint中的节点的isRepaintBoundary 必然为 true,换句话说,能被添加到 nodesNeedingPaint列表中节点都是绘制边界,那么这个边界究竟是如何起作用的,我们继续看 PaintingContext._repaintCompositedChild 函数的实现。

// flutter/packages/flutter/lib/src/rendering/object.dart
 static void _repaintCompositedChild(  // PaintingContext
    RenderObject child, {
    
    
    bool debugAlsoPaintedParent = false,
    PaintingContext? childContext,
  }) {
    
    
  assert(child.isRepaintBoundary); // 断言:能走的这里,其isRepaintBoundary必定为true.
  OffsetLayer? childLayer = child._layer as OffsetLayer?;
  if (childLayer == null) {
    
     //如果边界节点没有layer,则为其创建一个OffsetLayer
    child._layer = childLayer = OffsetLayer();
  } else {
    
      //如果边界节点已经有layer了(之前绘制时已经为其创建过layer了),则清空其子节点。
    childLayer.removeAllChildren();  
  }
  //通过其layer构建一个paintingContext,之后layer便和childContext绑定,这意味着通过同一个
  //paintingContext的canvas绘制的产物属于同一个layer。
  childContext ??= PaintingContext(child._layer!, child.paintBounds);
  child._paintWithContext(childContext, Offset.zero);  // 绘制子节点(树)
  childContext.stopRecordingIfNeeded(); // 停止记录工作
}

可以看到,在绘制边界节点时会首先检查其是否有 layer,如果没有就会创建一个新的 OffsetLayer 给它。

随后会根据该 offsetLayer 构建一个 PaintingContext 对象(记为childContext),

其次,childContext由参数传入,一般为null,因此会新建一个PaintingContext对象用于绘制该图层。之后子组件在获取contextcanvas对象时会创建一个 PictureLayer,然后再创建一个 Canvas 对象和新创建的 PictureLayer 关联起来,这意味着后续通过同一个childContextcanvas 绘制的产物属于同一个PictureLayer

_paintWithContext方法主要负责当前子节点图层的绘制。

最后,在完成所有子节点的绘制后,调用stopRecordingIfNeeded方法停止当前PaintingContext对象(即childContext)的记录工作。(Framework负责记录各种绘制指令,真正的绘制工作在Engine中进行 )

其中_paintWithContext方法代码如下:

void _paintWithContext(PaintingContext context, Offset offset) {
    
    
  if (_needsLayout)  return; // 异常情况:存在Layout未处理完的节点
  _needsPaint = false;
  try {
    
    
    paint(context, offset); // 开始绘制
    assert(!_needsLayout);  // Layout阶段完成
    assert(!_needsPaint);   // Paint阶段完成
  } catch (e, stack) {
    
     ...... }
}
void paint(PaintingContext context, Offset offset) {
    
     }

以上逻辑中,首先将_needsPaint字段标记为false,因为绘制即将开始,具体的绘制操作由RenderObject子类所实现的paint方法决定。该方法需要节点自己实现,用于绘制自身,节点类型不同,绘制算法一般也不同,不过功能是差不多的,即:如果是容器组件,要绘制孩子和自身(当然,容器自身也可能没有绘制逻辑,这种情况只绘制孩子即可,比如Center组件),如果不是容器类组件,则绘制自身(比如Image)。

RenderView为例,其最终将调用paintChild方法,代码如下:

// flutter/packages/flutter/lib/src/rendering/object.dart
void paintChild(RenderObject child, Offset offset) {
    
    
  if (child.isRepaintBoundary) {
    
     // 如果是绘制边界,则新建图层进行绘制
    stopRecordingIfNeeded();
    _compositeChild(child, offset);  
  } else {
    
     // 否则直接基于当前图层和上下文进行绘制
    child._paintWithContext(this, offset); 
  }
}

它的主要逻辑是:如果当前节点是边界节点,则停止当前图层的绘制,通过_compositeChild新建一个图层开始当前节点的绘制,如果不是,则调用前面分析过的 _paintWithContext 方法基于当前图层开始执行Paint逻辑。

其中,_compositeChild 方法如下:

// flutter/packages/flutter/lib/src/rendering/object.dart
void _compositeChild(RenderObject child, Offset offset) {
    
    
  assert(child.isRepaintBoundary); // 目标节点是绘制边界,否则不会进入本逻辑
  //如果子节点是边界节点,则递归调用repaintCompositedChild
  if (child._needsPaint) {
    
     
    repaintCompositedChild(child, debugAlsoPaintedParent: true);  // 创建一个新的Layer
  } else {
    
     ...... }
  assert(child._layer is OffsetLayer);
  //将孩子节点的layer添加到Layer树中,
  final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
  childOffsetLayer.offset = offset;
  //将当前边界节点的layer添加到父边界节点的layer中.
  appendLayer(child._layer!); // 加入Layer Tree
}

void appendLayer(Layer layer) {
    
     // 向Layer Tree中加入一个节点
  assert(!_isRecording);
  layer.remove();
  _containerLayer.append(layer); // 后面分析
}

以上逻辑首先会调用前面分析的repaintCompositedChild方法新建一个图层,并同步该图层的offset信息,即该图层从哪里开始绘制。最后调用appendLayer方法将新的图层加入当前的 Layer Tree。

这里需要注意三点:

  1. 绘制孩子节点时,如果遇到边界节点且当其不需要重绘(_needsPaintfalse) 时,会直接复用该边界节点的 layer,而无需重绘!这就是边界节点能跨 frame 复用的原理。
  2. 因为边界节点的layer类型是ContainerLayer,所以是可以给它添加子节点。
  3. 注意是将当前边界节点的 layer添加到 父边界节点,而不是父节点。

其中 _containerLayer字段的append方法如下:

// packages/flutter/lib/src/rendering/layer.dart
void append(Layer child) {
    
     // ContainerLayer
  adoptChild(child);
  child._previousSibling = lastChild; // 树结构的操作
  if (lastChild != null) lastChild!._nextSibling = child;
  _lastChild = child;
  _firstChild ??= child;
}

void adoptChild(AbstractNode child) {
    
    
  if (!alwaysNeedsAddToScene) {
    
     // 如果总是需要合成,则不需要尝试标记
    markNeedsAddToScene(); // 标记需要重新合成图层
  }
  super.adoptChild(child);
}

以上逻辑主要是完成子节点的挂载。在adoptChild方法中,因为大部分Layer节点的alwaysNeedsAddToScene属性均为false,故都会调用markNeedsAddToScene方法,表示当前节点需要加入Scene的构建。Scene是 Layer Tree 合成的最终产物。

按照上面的流程执行完毕后,最终所有边界节点的 layer 就会相连起来组成一棵 Layer Tree 。

RenderColoredBox 和 RenderOpacity 的 paint 方法分析

为了加深对Paint的理解,下面分析两个典型的RenderObject节点的paint方法,首先是_RenderColoredBoxpaint方法,如代码清单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); }
}

以上逻辑首先会绘制目标区域的颜色,然后绘制子节点。而RenderOpacitypaint方法则要复杂一些,如代码清单5-75所示。

// 代码清单5-75 flutter/packages/flutter/lib/src/rendering/proxy_box.dart

void paint(PaintingContext context, Offset offset) {
    
    
  if (child != null) {
    
    
    if (_alpha == 0) {
    
     // 全透明,相当于不存在
      layer = null;
      return; // 直接返回
    }
    if (_alpha == 255) {
    
     // 全不透明,即遮挡
      layer = null;
      context.paintChild(child!, offset); // 无需独立图层,直接绘制,相当于普通节点
      return;
    }
    assert(needsCompositing); // 新增一个半透明的Layer节点,见代码清单5-76
    layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as 
        OpacityLayer?);
  }
}

RenderOpacity只有在子节点存在时才会绘制,且全透明时直接返回,不透明时直接在当前PaintContext对象中进行绘制,只有半透明时才会通过pushOpacity方法在一个OpacityLayer 中进行绘制。

对于半透明的情况,最终将调用PaintingContextpushOpacity方法,如代码清单5-76所示。

// 代码清单5-76 flutter/packages/flutter/lib/src/rendering/object.dart
OpacityLayer pushOpacity(Offset offset, int alpha, 
    PaintingContextCallback painter, {
    
     OpacityLayer? oldLayer }) {
    
    
  final OpacityLayer layer = oldLayer ?? OpacityLayer();
  layer // 对于透明度图层,只需要知道Alpha的值和绘制偏移即可
    ..alpha = alpha
    ..offset = offset;
  pushLayer(layer, painter, Offset.zero); // 见代码清单5-77
  return layer;
}

以上逻辑将使用当前节点(RenderOpacity)的Layer,如果没有则新建一个OpacityLayer,并设置其透明度、偏移值等属性,然后将该Layer加入Layer Tree,如代码清单5-77所示。

// 代码清单5-77 flutter/packages/flutter/lib/src/rendering/object.dart
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter,
    Offset offset, {
    
     Rect? childPaintBounds }) {
    
    
  assert(painter != null); // 第1步,移除当前Layer的所有子节点
  if (childLayer.hasChildren) {
    
     childLayer.removeAllChildren(); } // 清空子节点
  stopRecordingIfNeeded(); // 第2步,停止当前Layer的绘制,见代码清单5-78
  appendLayer(childLayer); // 加入Layer Tree,见代码清单5-73
  final PaintingContext childContext = // 第3步,创建新图层的PaintingContext
        createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
  painter(childContext, offset); // 开始新图层的绘制,见代码清单5-79
  childContext.stopRecordingIfNeeded(); // 新图层绘制完成,见代码清单5-78
}

PaintingContext createChildContext( ...... ) {
    
    
  return PaintingContext(childLayer, bounds);
}

以上逻辑中,第1步,移除当前Layer的所有子节点。第2步,停止当前Layer的绘制,如代码清单5-78,然后调用appendLayer方法将当前Layer加入Layer Tree,核心逻辑如代码清单5-73所示。第3步,创建一个新的PaintingContext对象用于新图层的绘制,painter由具体的RenderObject节点传入,对于RenderOpacity,其painter方法如代码清单5-79所示。最后,结束当前图层的绘制并退出。

// 代码清单5-78 flutter/packages/flutter/lib/src/rendering/object.dart

 // 重置相关变量
void stopRecordingIfNeeded() {
    
    
  if (!_isRecording) return;
  _currentLayer!.picture = _recorder!.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}

以上逻辑用于结束一个Layer的绘制,主要是将相关字段设置为nullendRecording方法最终将调用Engine中的一个方法。

接下来分析OpacityLayer真正的绘制逻辑,如代码清单5-79所示。

// 代码清单5-79 flutter/packages/flutter/lib/src/rendering/proxy_box.dart

mixin RenderProxyBoxMixin<T extends RenderBox> 
            on RenderBox, RenderObjectWithChildMixin<T> {
    
    
  
  void paint(PaintingContext context, Offset offset) {
    
    
    if (child != null) context.paintChild(child!, offset); // 见代码清单5-71
  }
}

以上逻辑主要是对子节点进行绘制,paintChild方法的逻辑在前面已介绍过。RenderOpacity只负责提供带透明效果的Layer,而RenderParagraph需要基于Canvas进行绘制,其get方法如代码清单5-80所示。

// 代码清单5-80 flutter/packages/flutter/lib/src/rendering/object.dart
 // PaintingContext
Canvas get canvas {
    
     // 见代码清单5-74,绘制时使用
  if (_canvas == null)  _startRecording();  //如果canvas为空,则是第一次获取 
  return _canvas!;
}
// 创建PictureLayer和canvas
void _startRecording() {
    
    
  assert(!_isRecording);
  _currentLayer = PictureLayer(estimatedBounds);
  _recorder = ui.PictureRecorder(); // 记录所有的绘制指令
  _canvas = Canvas(_recorder!); // 创建 canvas
  //将pictureLayer添加到_containerLayer(是绘制边界节点的Layer)中
  _containerLayer.append(_currentLayer!);
}

由以上逻辑可知,真正的绘制是在PictureLayer中进行的,PictureRecorder负责保存所有的绘制指令。

需要注意的是,以上逻辑中CanvasPictureRecorder等都继承自NativeFieldWrapperClass2 类,该类是Dart提供的用于封装Native(C++)对象的父类,故以上绘制操作(通过Canvas,借由PictureRecorder记录)都是Native调用,这些调用将被合成为最终的上屏数据。

创建新的 PictureLayer

现在,我们在本节最开篇示例基础上,给 Row 添加第三个子节点 Text5,如图,那么它的Layer 树会变成什么样的?

在这里插入图片描述

因为 Text5 是在 RepaintBoundary 绘制完成后才会绘制,上例中当 RepaintBoundary 的子节点绘制完时,将 RepaintBoundarylayerOffsetLayer2 )添加到父级LayerOffsetLayer1)中后发生了什么?答案在我们上面介绍的repaintCompositedChild 的最后一行:

...
childContext.stopRecordingIfNeeded(); 

我们看看其删减后的核心代码:

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 如图:

在这里插入图片描述

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

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

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

在这里插入图片描述

左边是一个 Stack 布局,右边是对应的 Layer Tree 结构;我们知道Stack布局中会根据其子组件的加入顺序进行层叠绘制,最先加入的孩子在最底层,最后加入的孩子在最上层。可以设想一下如果绘制 Child3 时复用了 PictureLayer1,则会导致 Child3Child2 遮住,这显然不符合预期,但如果新建一个 PictureLayer 在添加到 OffsetLayer 最后面,则可以获得正确的结果。

现在我们再来深入思考一下:如果 Child2 的父节点不是 RepaintBoundary,那么是否就意味着 Child3Child1就可以共享同一个 PictureLayer 了?

  • 答案是否定的!如果 Child2 的父组件改为一个自定义的组件,在这个自定义的组件中我们希望对子节点在渲染时进行一些矩阵变化,为了实现这个功能,我们创建一个新的 TransformLayer 并指定变换规则,然后我们把它传递给 Child2Child2会绘制完成后,我们需要将 TransformLayer 添加到 Layer 树中(不添加到Layer树中是不会显示的),则组件树和最终的 Layer Tree 结构如图所示:

在这里插入图片描述

可以发现这种情况本质上和上面使用 RepaintBoudary 的情况是一样的,Child3 仍然不应该复用 PictureLayer1,那么现在我们可以总结一个一般规律了:只要一个组件需要往 Layer 树中添加新的 Layer,那么就必须也要结束掉当前 PictureLayer 的绘制。 这也是为什么 PaintingContext 中需要往 Layer 树中添加新 Layer 的方法(比如pushLayeraddLayer)中都有如下两行代码:

stopRecordingIfNeeded(); //先结束当前 PictureLayer 的绘制
appendLayer(layer);// 再添加到 layer树

这是向 Layer 树中添加Layer的标准操作。这个结论要牢记,我们在后面介绍 flushCompositingBits() 的原理时会用到。

综上,Layer树的最终结构大致如图所示(随便一个例子,并不和本例对应):

在这里插入图片描述

compositeFrame

创建好layer后,接下来就需要上屏展示了,而这部分工作是由renderView.compositeFrame方法来完成的。实际上他的实现逻辑很简单:先通过layer构建Scene,最后再通过window.render API 来渲染:

void compositeFrame() {
    
    
	final ui.SceneBuilder builder = ui.SceneBuilder();
	final ui.Scene scene = layer!.buildScene(builder);
	window.render(scene);
	...
}

这里值得一提的是构建Scene的过程,我们看一下核心源码:

ui.Scene buildScene(ui.SceneBuilder builder) {
    
    
  updateSubtreeNeedsAddToScene();
  addToScene(builder); //关键
  final ui.Scene scene = builder.build();
  return scene;
}

其中最关键的一行就是调用addToScene,该方法主要的功能就是将 Layer Tree 中每一个layer传给 Skia(最终会调用native API,如果想了解详情,建议查看 OffsetLayerPictureLayeraddToScene 方法),这是上屏前的最后一个准备动作,最后就是调用 window.render 将绘制数据发给GPU渲染出来了!( window.render 会调用Native层Engine的Render方法进行渲染工作,这里不做展开分析)

Layer 使用实例

本节通过优化之前“绘制棋盘示例“来像大家展示如何在自定义组件中使用Layer

通过 Layer 实现绘制缓存

我们之前绘制棋盘示例是使用的CustomPaint组件,然后再painterpaint方法中同时实现了绘制棋盘和棋子,实际上这里可以有一个优化,因为棋盘是不会变化的,所以理想的方式就是当绘制区域不发生变化时,棋盘只需要绘制一次,当棋子发生变化时,每次只需要绘制棋子信息即可。

注意:在实际开发中,要实现上述功能还是优先使用Flutter建议的”Widget组合“的方式:比如棋盘和棋子分别绘制在两个Widget中,然后包上 RepaintBoundary 组件后把他们添加到 Stack中,这样做到分层渲染。不过,本节主要是为了说明Flutter自定义组件中如何使用Layer,所以我们采用自定义RenderObject的方式来实现。

  1. 首先我们定义一个ChessWidget,因为它并非容器类组件,所以继承自 LeafRenderObjectWidget :
class ChessWidget extends LeafRenderObjectWidget {
    
    
  
  RenderObject createRenderObject(BuildContext context) {
    
     
    return RenderChess(); // 返回Render对象
  }
  //...省略updateRenderObject函数实现
}

由于自定义的 RenderChess 对象不接受任何参数,所以我们可以在ChessWidget 中不用实现updateRenderObject方法。

  1. 实现 RenderChess;我们先直接实现一个未缓存棋盘的原始版本,随后我们再一点点添加代码,直到把它改造成可以缓存棋盘的对象。
class RenderChess extends RenderBox {
    
    
  
  void performLayout() {
    
    
    //确定ChessWidget的大小
    size = constraints.constrain(
      constraints.isTight ? Size.infinite : Size(150, 150),
    );
  }

  
  void paint(PaintingContext context, Offset offset) {
    
    
    Rect rect = offset & size;
    drawChessboard(canvas, rect); // 绘制棋盘
    drawPieces(context.canvas, rect);//绘制棋子
  }
}
  1. 接下来我们需要实现棋盘缓存,我们的思路是:
  • 创建一个 Layer 专门绘制棋盘,然后缓存。
  • 当重绘触发时,如果绘制区域发生了变化,则重新绘制棋盘并缓存;如果绘制区域未变,则直接使用之前的Layer

为此,我们需要定义一个PictureLayer来缓存棋盘,然后添加一个 _checkIfChessboardNeedsUpdate 函数来实现上述逻辑:

// 保存之前的棋盘大小
Rect _rect = Rect.zero;
PictureLayer _layer = PictureLayer()

_checkIfChessboardNeedsUpdate(Rect rect) {
    
    
  // 如果绘制区域大小没发生变化,则无需重绘棋盘
  if (_rect == rect) return;
  
  // 绘制区域发生了变化,需要重新绘制并缓存棋盘
  _rect = rect;
  print("paint chessboard");
 
  // 新建一个PictureLayer,用于缓存棋盘的绘制结果,并添加到layer中
  ui.PictureRecorder recorder = ui.PictureRecorder();
  Canvas canvas = Canvas(recorder);
  drawChessboard(canvas, rect); //绘制棋盘
  // 将绘制产物保存在pictureLayer中
  _layer = PictureLayer(Rect.zero)..picture = recorder.endRecording();
}


void paint(PaintingContext context, Offset offset) {
    
    
  Rect rect = offset & size;
  //检查棋盘大小是否需要变化,如果变化,则需要重新绘制棋盘并缓存
  _checkIfChessboardNeedsUpdate(rect);
  //将缓存棋盘的layer添加到context中,每次重绘都要调用,原因下面会解释
  context.addLayer(_layer);
  //再画棋子
  print("paint pieces");
  drawPieces(context.canvas, rect);
}

具体的实现逻辑见注释,这里不再赘述,需要特别解释的是在 paint 方法中,每次重绘都需要调用 context.addLayer(_layer) 将棋盘layer添加到当前的 Layer Tree 中,通过上一节的介绍,我们知道,实际上是添加到了当前节点的第一个绘制边界节点的Layer中。可能会有人疑惑,如果棋盘不变的话,添加一次不就行了,为什么每次重绘都要添加?实际上这个问题我们上一节已经解释过了,因为重绘是当前节点的第一个父级向下发起的,而每次重绘前,该节点都会先清空所有的孩子,代码见 PaintingContext.repaintCompositedChild 方法,所以我们需要每次重绘时都要添加一下。

OK,现在我们已经实现了棋盘缓存了,下面我们来验证一下。

我们创建一个测试 Demo 来验证一下,我们创建一个 ChessWidget 和一个 ElevatedButton,因为ElevatedButton在点击时会执行水波动画,所以会发起一连串的重绘请求,而根据上一节的知识,我们知道ChessWidgetElevatedButton 会在同一个Layer上绘制,所以 ElevatedButton 重绘也会导致ChessWidget 的重绘。另外我们在绘制棋子和棋盘时都加了日志,所以我们只需要点击 ElevatedButton,然后查看日志就能验证棋盘缓存是否生效。

注意:在当前版本(3.0)的Flutter中,ElevatedButton 的实现中并没有添加 RepaintBoundary,所以它才会和ChessWidget 在同一个 Layer 上渲染,如果后续 Flutter SDK中给 ElevatedButton 添加了RepaintBoundary,则不能通过本例来验证。

class PaintTest extends StatefulWidget {
    
    
  const PaintTest({
    
    Key? key}) : super(key: key);

  
  State<PaintTest> createState() => _PaintTestState();
}

class _PaintTestState extends State<PaintTest> {
    
    
  ByteData? byteData;

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const ChessWidget(),
          ElevatedButton(
            onPressed: () {
    
    
              setState(() => null);
            },
            child: Text("setState"),
          ),
        ],
      ),
    );
  }
}

点击按钮后发现,棋盘、棋子都可以正常显示,如图:

图14-16

同时日志面板输出了很多"paint pieces",并没有"paint chessboard",可见棋盘缓存生效了。

好的,貌似我们预期的功能已经实现了,但是别高兴太早,上面的代码还有一个内存泄露的坑,我们在下面 LayerHandle 部分介绍。

LayerHandle

上面 RenderChess 实现中,我们将棋盘绘制信息缓存到了 layer 中,因为 layer 中保存的绘制产物是需要调用 dispose 方法释放的,如果ChessWidget销毁时没有释放则会发生内存泄露,所以们需要在组件销毁时,手动释放一下,给RenderChess中添加如下代码:


void dispose() {
    
    
  _layer.dispose();
  super.dispose();
}

上面的场景比较简单,实际上,在Flutter中一个layer可能会反复被添加到多个容器类Layer中,或从容器中移除,这样一来有些时候我们可能会搞不清楚一个layer是否还被使用,为了解决这个问题,Flutter中定义了一个LayerHandle 类来专门管理layer,内部是通过引用计数的方式来跟踪layer是否还有使用者,一旦没有使用者,会自动调用layer.dispose来释放资源。

为了符合Flutter规范,强烈建议在需要使用layer的时候通过LayerHandle来管理它。现在我们来修改一下上面的代码,RenderChess中定义一个 layerHandle,然后将_layer 全部替换为 layerHandle.layer

// 定义一个新的 layerHandle
final layerHandle = LayerHandle<PictureLayer>();
 
_checkIfChessboardNeedsUpdate(Rect rect) {
    
    
    ...
    layerHandle.layer = PictureLayer(Rect.zero)..picture = recorder.endRecording();
  }

  
  void paint(PaintingContext context, Offset offset) {
    
    
    ...
    //将缓存棋盘的layer添加到context中
    context.addLayer(layerHandle.layer!);
    ...
  }

  
  void dispose() {
    
    
    //layer通过引用计数的方式来跟踪自身是否还被layerHandle持有,
    //如果不被持有则会释放资源,所以我们必须手动置空,该set操作会
    //解除layerHandle对layer的持有。
    layerHandle.layer = null;
    super.dispose();
  }

OK,这样就很好了!不过先别急着庆祝,现在我们再来回想一下上一节介绍的内容,每一个 RenderObject 都有一个layer 属性,我们能否直接使用它来保存棋盘layer呢?下面我们看看 RenderObject 中关于 layer 的定义:


set layer(ContainerLayer? newLayer) {
    
    
  _layerHandle.layer = newLayer;
}

final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();

可以发现,我们RenderObject 中已经定义了一个 _layerHandle了,它会去管理 layer;同时 layer 是一个setter,会自动将新 layer 赋值到 _layerHandle 上,那么我们是否可以在 RenderChess 中直接使用父类定义好的 _layerHandle,这样的话我们就无需再自定义一个 layerHandle 了。

答案是:取决于当前节点的 isRepaintBoundary 属性是否为 true(即当前节点是否为绘制边界节点) ,如果为 true 则不可以,如果不为 true,则可以。上一节中讲过,Flutter在执行 flushPaint 重绘时遇到绘制边界节点:

  1. 先检查其 layer 是否为空,如果不为空,则会先清空该 layer 的孩子节点,然后会使用该 layer 创建一个 PaintingContext,传递给 paint 方法。
  2. 如果其 layer 为空,会创建一个 OffsetLayer 给它。

如果我们要将棋盘layer保存到预定义的 layer变量中的话,得先创建一个ContainerLayer,然后将绘制棋盘的PictureLayer作为子节点添加到新创建的ContainerLayer中,然后赋值给 layer变量。这样一来:

  1. 如果我们设置 RenderChessisRepaintBoundarytrue,那么在每次重绘时,flutter 框架都会将 layer 子节点清空,这样的话,我们的棋盘Picturelayer就会被移除,接下来就会触发异常。
  2. 如果 RenderChessisRepaintBoundaryfalse(默认值),则在重绘过程中 flutter 框架不会使用到 layer 属性,这中情况没有问题。

虽然,本例中 RenderChessisRepaintBoundaryfalse,直接使用 layer是可以的,但我不建议这么做,原因有二:

  1. RenderObject 中的 layer 字段在 Flutter 框架中是专门为绘制流程而设计的,按照职责分离原则,我们也不应该去蹭它。即使现在能蹭成功,万一哪天Flutter的绘制流发生变化,比如也开始使用非绘制边界节点的layer字段,那么我们的代码将会出问题。
  2. 如果要使用Layer,我们也需要先创建一个ContainerLayer,既然如此,我们还不如直接创建一个LayerHandle,这更方便。

现在考虑最后一个问题,在上面示例中,我们点击按钮后,虽然棋盘不会重绘了,但棋子还是会重绘,这并不合理,我们希望棋盘区域不受外界干扰,只有新的落子行为时(点击在棋盘区域)时再重绘棋子。相信看到着,解决方案就呼之欲出了,我们有两种选择:

  1. RenderChessisRepaintBoundary 返回 true;将当前节点变为一个绘制边界,这样 ChessWidget 就会和按钮分别在不同的 layer 上绘制,也就不会相互影响。
  2. 在使用 ChessWidget 时,给它套一个RepaintBoundary组件,和 1 的原理差不多的,只不过这种方式是将ChessWidget的父节点(RepaintBoundary)变为了绘制边界(而不是自身),这样也会创建一个新的 layer 来隔离按钮的绘制。

具体应该选哪种应该根据情况而定,第二种方案会更灵活,但第一种方案的实际效果往往会比较好,因为如果我们封装的复杂自绘控件中没有设置 isRepaintBoundarytrue,我们很难保证使用者在使用时会给我们的控件添加RepaintBoundary,所以这种细节还是对使用者屏蔽掉会比较好。

Compositing

下面我们来介绍一下 flushCompositingBits()。现在,我们再来回顾一下Flutter的渲染管线:

void drawFrame(){
    
    
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame()
  ...//省略  
} 

其中只有 flushCompositingBits() 还没有介绍过,这是因为要理解flushCompositingBits(),就必须的了解Layer是什么,以及 Layer 树构建的过程。为了更容易理解它,我们先看一个demo。

CustomRotatedBox

我们实现一个CustomRotatedBox,它的功能是将其子元素放倒(顺时针旋转 90 度),要实现个效果我们可以直接使用 canvas 的变换功能,下面是核心代码:

class CustomRotatedBox extends SingleChildRenderObjectWidget {
    
    
  CustomRotatedBox({
    
    Key? key, Widget? child}) : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return CustomRenderRotatedBox();
  }
}

class CustomRenderRotatedBox extends RenderBox
    with RenderObjectWithChildMixin<RenderBox> {
    
    

  
  void performLayout() {
    
    
    _paintTransform = null;
    if (child != null) {
    
    
      child!.layout(constraints, parentUsesSize: true);
      size = child!.size;
      //根据子组件大小计算出旋转矩阵
      _paintTransform = Matrix4.identity()
        ..translate(size.width / 2.0, size.height / 2.0)
        ..rotateZ(math.pi / 2) // 旋转90度
        ..translate(-child!.size.width / 2.0, -child!.size.height / 2.0);
    } else {
    
    
      size = constraints.smallest;
    }
  }

  
  void paint(PaintingContext context, Offset offset) {
    
    
    if(child!=null){
    
    
       // 根据偏移,需要调整一下旋转矩阵
        final Matrix4 transform =
          Matrix4.translationValues(offset.dx, offset.dy, 0.0)
            ..multiply(_paintTransform!)
            ..translate(-offset.dx, -offset.dy);
      _paint(context, offset, transform);
    } else {
    
    
      //...
    }
  }
  
 void _paint(PaintingContext context,Offset offset,Matrix4 transform ){
    
    
    // 为了不干扰其他和自己在同一个layer上绘制的节点,所以需要先调用save然后在子元素绘制完后
    // 再调用restore显示,关于save/restore有兴趣可以查看Canvas API doc
    context.canvas
      ..save()
      ..transform(transform.storage);
    context.paintChild(child!, offset);
    context.canvas.restore();
  }
  ... //省略无关代码
}

下面我们写个demo测试一下:

class CustomRotatedBoxTest extends StatelessWidget {
    
    
  const CustomRotatedBoxTest({
    
    Key? key}) : super(key: key);

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

运行效果如图,A 被成功放倒了:

在这里插入图片描述
现在我们给 CustomRotatedBox 添加一个 RepaintBoundary 再试试:


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

运行后如图:

在这里插入图片描述

咦,A 怎么又站起来了!

我们来分析一下原因:根据上一节的知识,我们可以很容易画出添加 RepaintBoundary 前和后的 Layer Tree 结构,如图:

在这里插入图片描述

添加 RepaintBoundary 后,CustomRotatedBox 中的持有的还是 OffsetLayer1

void _paint(PaintingContext context,Offset offset,Matrix4 transform ){
    
    
    context.canvas // 该 canvas 对应的是 PictureLayer1 
      ..save()
      ..transform(transform.storage);
    // 子节点是绘制边界节点,会在新的 OffsetLayer2中的 PictureLayer2 上绘制
    context.paintChild(child!, offset); 
    context.canvas.restore();
  }
  ... //省略无关代码
}

很显然,CustomRotatedBox 中进行旋转变换的 canvas 对应的是 PictureLayer1,而 Text("A") 的绘制是使用的PictureLayer2 对应的 canvas ,他们属于不同的 Layer。可以发现父子的 PictureLayer “分离了”,所以CustomRotatedBox 也就不会对 Text("A") 起作用。那么如何解决这个问题呢?

我们在前面的小节介绍过,有很多容器类组件都附带变换效果,拥有旋转变换的容器类 Layer 是 TransformLayer,那么我们就可以在 CustomRotatedBox 中绘制子节点之前:

  1. 创建一个TransformLayer(记为 TransformLayer1) 添加到 Layer树中,接着创建一个新的 PaintingContextTransformLayer1绑定。
  2. 子节点通过这个新的 PaintingContext 去绘制。

完成上述操作之后,后代节点绘制所在的 PictureLayer 都会是 TransformLayer 的子节点,因此我们可以通过 TransformLayer 对所有子节点整体做变换。下图是添加是 TransformLayer1前、后的 Layer 树结构。

在这里插入图片描述

这其实就是一个重新 Layer 合成(layer compositing) 的过程:创建一个新的 ContainerLayer,然后将该ContainerLayer 传递给子节点,这样后代节点的Layer必然属于ContainerLayer ,那么给这个 ContainerLayer 做变换就会对其全部的子孙节点生效。

“Layer 合成” 在不同的语境会有不同的指代,比如 skia 最终渲染时也是将一个个 layer 渲染出来,这个过程也可以认为是多个 layer 上的绘制信息合成为最终的位图信息;另外 canvas 中也有 layer 的概念(canvas.save 方法生成新的layer),对应的将所有layer 绘制结果最后叠加在一起的过程也可以成为 layer 合成。

下面我们看看具体代码实现。由于 Layer 的组合是一个标准的过程(唯一的不同是使用哪种ContainerLayer来作为父容器),PantingContext 中提供了一个 pushLayer 方法来执行组合过程,我们看看其实现源码:

void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, {
    
     Rect? childPaintBounds }) {
    
    
  
  if (childLayer.hasChildren) {
    
    
    childLayer.removeAllChildren();
  }
  //下面两行是向Layer树中添加新Layer的标准操作,在之前小节中详细介绍过,忘记的话可以去查阅。
  stopRecordingIfNeeded();
  appendLayer(childLayer);
  
  //通过新layer创建一个新的childContext对象
  final PaintingContext childContext = 
    createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
  //painter是绘制子节点的回调,我们需要将新的childContext对象传给它
  painter(childContext, offset);
  //子节点绘制完成后获取绘制产物,将其保存到PictureLayer.picture中
  childContext.stopRecordingIfNeeded();
}

那么,我们只需要创建一个 TransformLayer 然后指定我们需要的旋转变换,然后直接调用 pushLayer 可以:

// 创建一个持有 TransformLayer 的 handle.
final LayerHandle<TransformLayer> _transformLayer = LayerHandle<TransformLayer>();

void _paintWithNewLayer(PaintingContext context, Offset offset, Matrix4 transform) {
    
    
    //创建一个 TransformLayer,保存在handle中
    _transformLayer.layer = _transformLayer.layer ?? TransformLayer();
    _transformLayer.layer!.transform = transform;
    
    context.pushLayer(
      _transformLayer.layer!,
      _paintChild, // 子节点绘制回调;添加完layer后,子节点会在新的layer上绘制
      offset,
      childPaintBounds: MatrixUtils.inverseTransformRect(
        transform,
        offset & size,
      ),
    );
 }

 // 子节点绘制回调 
 void _paintChild(PaintingContext context, Offset offset) {
    
    
   context.paintChild(child!, offset);
 }

然后我们需要在 paint 方法中判断一下子节点是否是绘制边界节点,如果是则需要走layer组合,如果不是则需要走 layer 合成:

 
 void paint(PaintingContext context, Offset offset) {
    
    
    if (child != null) {
    
    
      final Matrix4 transform =
          Matrix4.translationValues(offset.dx, offset.dy, 0.0)
            ..multiply(_paintTransform!)
            ..translate(-offset.dx, -offset.dy);
      
      if (child!.isRepaintBoundary) {
    
     // 添加判断
        _paintWithNewLayer(context, offset, transform);
      } else {
    
    
        _paint(context, offset, transform);
      }
    } else {
    
    
      _transformLayer.layer = null;
    }
 }

为了让代码看起看更清晰,我们将child不为空时的绘制逻辑逻辑封装一个 pushTransform函数里:

  TransformLayer? pushTransform(
    PaintingContext context,
    bool needsCompositing,
    Offset offset,
    Matrix4 transform,
    PaintingContextCallback painter, {
    
    
    TransformLayer? oldLayer,
  }) {
    
    
    
    final Matrix4 effectiveTransform =
        Matrix4.translationValues(offset.dx, offset.dy, 0.0)
          ..multiply(transform)
          ..translate(-offset.dx, -offset.dy);
    
    if (needsCompositing) {
    
    
      final TransformLayer layer = oldLayer ?? TransformLayer();
      layer.transform = effectiveTransform;
      context.pushLayer(
        layer,
        painter,
        offset,
        childPaintBounds: MatrixUtils.inverseTransformRect(
          effectiveTransform,
          context.estimatedBounds,
        ),
      );
      return layer;
    } else {
    
    
      context.canvas
        ..save()
        ..transform(effectiveTransform.storage);
      painter(context, offset);
      context.canvas.restore();
      return null;
    }
  }

然后修改一下 paint 实现,直接调用 pushTransform 方法即可:


void paint(PaintingContext context, Offset offset) {
    
    
  if (child != null) {
    
    
    pushTransform(
      context,
      child!.isRepaintBoundary,
      offset,
      _paintTransform!,
      _paintChild,
      oldLayer: _transformLayer.layer,
    );
  } else {
    
    
    _transformLayer.layer = null;
  }
}

是不是清晰多了,现在我们重新运行一下示例,效果与前面一样,A被成功放倒了!

需要说明的是,其实 PaintingContext 已经帮我们封装好了 pushTransform 方法,我们可以直接使用它:


void paint(PaintingContext context, Offset offset) {
    
    
  if (child != null) {
    
    
    context.pushTransform(
      child!.isRepaintBoundary,
      offset,
      _paintTransform!,
      _paintChild,
      oldLayer: _transformLayer.layer,
    );
  } else {
    
    
    _transformLayer.layer = null;
  }
}

实际上,PaintingContext 针对常见的拥有变换功能的容器类Layer的组合都封装好了相应的方法,同时Flutter中已经预定了拥有相应变换功能的组件,下面是一个对应表:

Layer的名称 PaintingContext对应的方法 Widget
ClipPathLayer pushClipPath ClipPath
OpacityLayer pushOpacity Opacity
ClipRRectLayer pushClipRRect ClipRRect
ClipRectLayer pushClipRect ClipRect
TransformLayer pushTransform RotatedBox、Transform

什么时候需要合成 Layer ?

1. 合成 Layer 的原则

通过上面的例子我们知道 CustomRotatedBox 的直接子节点是绘制边界节点时 CustomRotatedBox 中就需要合成 layer。实际上这只是一种特例,还有一些其他情况也需要 CustomRotatedBox 进行 Layer 合成,那什么时候需要 Layer 合成有没有一个一般性的普适原则?答案是:有! 我们思考一下 CustomRotatedBox 中需要 Layer 合成的根本原因是什么?如果 CustomRotatedBox 的所有后代节点都共享的是同一个PictureLayer,但是,一旦有后代节点创建了新的PictureLayer,则绘制就会脱离了之前PictureLayer,因为不同的PictureLayer上的绘制是相互隔离的,是不能相互影响,所以为了使变换对所有后代节点对应的 PictureLayer 都生效,则我们就需要将所有后代节点的添加到同一个 ContainerLayer 中,所以就需要在 CustomRotatedBox 中先进行 Layer 合成。

综上,一个普适的原则就呼之欲出了:当后代节点会向 layer 树中添加新的绘制类Layer时,则父级的变换类组件中就需要合成 Layer。

下面我们验证一下:

现在我们修改一下上面的示例,给 RepaintBoundary 添加一个 Center 父组件:


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

因为 CustomRotatedBox 中只判断了其直接子节点的child!.isRepaintBoundarytrue时,才会进行 layer 合成,而现在它的直接子节点是Center,所以该判断会是false,则不会进行 layer 合成。但是根据我们上面得出的结论,RepaintBoundary 作为CustomRotatedBox 的后代节点且会向 layer 树中添加新 layer 时就需要进行 layer合成,而本例中是应该合成layer但实际上却没有合成,所以预期是不能将 “A” 放倒的,运行后发现效果是 ”A“ 果然并没有被放倒!

在这里插入图片描述

看来我们的 CustomRotatedBox 还是需要继续修改。解决这个问题并不难,我们在判断是否需要进行 Layer 合成时,要去遍历整个子树,看看否存在绘制边界节点,如果是则合成,反之则否。为此,我们新定义一个在子树上查找是否存在绘制边界节点的 needCompositing() 方法:

//子树中递归查找是否存在绘制边界
needCompositing() {
    
    
  bool result = false;
  _visit(RenderObject child) {
    
    
    if (child.isRepaintBoundary) {
    
    
      result = true;
      return ;
    } else {
    
    
      //递归查找
      child.visitChildren(_visit);
    }
  }
  //遍历子节点
  visitChildren(_visit);
  return result;
}

然后需要修改一下 paint 实现:


void paint(PaintingContext context, Offset offset) {
    
    
  if (child != null) {
    
    
    context.pushTransform(
      needCompositing(), //子树是否存在绘制边界节点
      offset,
      _paintTransform!,
      _paintChild,
      oldLayer: _transformLayer.layer,
    );
  } else {
    
    
    _transformLayer.layer = null;
  }
}

现在,我们再来运行一下demo,运行后效果:

在这里插入图片描述

又成功放倒了!但还有问题,我们继续往下看。

2. alwaysNeedsCompositing

我们考虑一下这种情况:如果 CustomRotatedBox 的后代节点中没有绘制边界节点,但是有后代节点向 layer 树中添加了新的 layer。这种情况下,按照我们之前得出的结论 CustomRotatedBox 中也是需要进行 layer 合成的,但 CustomRotatedBox 实际上并没有。问题知道了,但是这个问题却不好解决,原因是我们在 CustomRotatedBox 中遍历后代节点时,是无法知道非绘制边界节点是否往 layer 树中添加了新的 layer。怎么办呢?Flutter是通过约定来解决这个问题的:

  1. RenderObject 中定义了一个布尔类型 alwaysNeedsCompositing 属性。

  2. 约定:自定义组件中,如果组件 isRepaintBoundaryfalse 时,在绘制时要会向 layer 树中添加新的 layer的话,要将 alwaysNeedsCompositing 置为 true

开发者在自定义组件时应该遵守这个规范。根据此规范,CustomRotatedBox 中我们在子树中递归查找时的判断条件就可以改为:

child.isRepaintBoundary || child.alwaysNeedsCompositing

最终 我们的needCompositing 实现如下:

 //子树中递归查找是否存在绘制边界
 needCompositing() {
    
    
    bool result = false;
    _visit(RenderObject child) {
    
    
      // 修改判断条件改为
      if (child.isRepaintBoundary || child.alwaysNeedsCompositing) {
    
    
        result = true;
        return ;
      } else {
    
    
        child.visitChildren(_visit);
      }
    }
    visitChildren(_visit);
    return result;
  }

注意:这要求非绘制节点组件在向 layer 树中添加 layer 时必须的让自身的 alwaysNeedsCompositing 值为 ture .

下面我们看一下 flutter 中 Opacity 组件的实现。

3. Opacity 解析

Opacity 可以对子树进行透明度控制,这个效果通过 canvas 是很难实现的,所以 flutter 中直接使用了 OffsetLayer 合成的方式来实现:

class RenderOpacity extends RenderProxyBox {
    
    
  
  // 本组件是非绘制边界节点,但会在部分透明的情况下向layer树中添加新的Layer,所以部分透明时要返回 true
  
  bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);
  
    
  void paint(PaintingContext context, Offset offset) {
    
    
    if (child != null) {
    
    
      if (_alpha == 0) {
    
    
        // 完全透明,则没必要再绘制子节点了
        layer = null;
        return;
      }
      if (_alpha == 255) {
    
    
        // 完全不透明,则不需要变换处理,直接绘制子节点即可
        layer = null;
        context.paintChild(child!, offset);
        return;
      }
      // 部分透明,需要通过OffsetLayer来处理,会向layer树中添加新 layer
      layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
    }
  }
}  

4. 优化

注意,上面我们通过 CustomRotatedBox 演示了变换类组件的核心原理,不过还有一些优化的地方,比如:

  1. 变换类组件中,遍历子树以确定是否需要 layer 合成是变换类组件的通用逻辑,不需要在每个组件里都实现一遍。
  2. 不是每一次重绘都需要去遍历子树,比如可以在初始化时遍历一次,然后将结果缓存,如果后续有变化,再重新遍历更新即可,此时直接使用缓存的结果。

Flutter 也考虑到了这个问题,于是便有了flushCompositingBits 方法,我们下面来正式介绍它。

flushCompositingBits

每一个节点(RenderObject中)都有一个_needsCompositing 字段,该字段用于缓存当前节点在绘制子节点时是否需要合成 layer。flushCompositingBits 的功能就是在节点树初始化和子树中合成信息发生变化时来重新遍历节点树,更新每一个节点的_needsCompositing 值。可以发现:

递归遍历子树的逻辑抽到了 flushCompositingBits 中,不需要组件单独实现。
不需要每一次重绘都遍历子树了,只需要在初始化和发生变化时重新遍历。
完美的解决了我们之前提出的问题,下面我们看一下具体实现:

void flushCompositingBits() {
    
    
  // 对需要更新合成信息的节点按照节点在节点树中的深度排序
  _nodesNeedingCompositingBitsUpdate.sort((a,b) => a.depth - b.depth);
  for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
    
    
    if (node._needsCompositingBitsUpdate && node.owner == this)
      node._updateCompositingBits(); //更新合成信息
  }
  _nodesNeedingCompositingBitsUpdate.clear();
}

RenderObject_updateCompositingBits 方法的功能就是递归遍历子树确定如果每一个节点的_needsCompositing 值:

void _updateCompositingBits() {
    
    
  if (!_needsCompositingBitsUpdate)
    return;
  final bool oldNeedsCompositing = _needsCompositing;
  _needsCompositing = false;
  // 递归遍历查找子树, 如果有孩子节点 needsCompositing 为true,则更新 _needsCompositing 值
  visitChildren((RenderObject child) {
    
    
    child._updateCompositingBits(); //递归执行
    if (child.needsCompositing)
      _needsCompositing = true;
  });
  // 这行我们上面讲过
  if (isRepaintBoundary || alwaysNeedsCompositing)
    _needsCompositing = true;
  if (oldNeedsCompositing != _needsCompositing)
    markNeedsPaint();
  _needsCompositingBitsUpdate = false;
}

执行完毕后,每一个节点的_needsCompositing 就确定了,我们在绘制时只需要判断一下当前的 needsCompositing(一个getter,会直接返回_needsCompositing ) 就能知道子树是否存在剥离layer了。这样的话,我们可以再优化一下 CustomRenderRotatedBox 的实现,最终的实现如下:

class CustomRenderRotatedBox extends RenderBox
    with RenderObjectWithChildMixin<RenderBox> {
    
    
  Matrix4? _paintTransform;

  
  void performLayout() {
    
    
    _paintTransform = null;
    if (child != null) {
    
    
      child!.layout(constraints, parentUsesSize: true);
      size = child!.size;
      //根据子组件大小计算出旋转矩阵
      _paintTransform = Matrix4.identity()
        ..translate(size.width / 2.0, size.height / 2.0)
        ..rotateZ(math.pi / 2)
        ..translate(-child!.size.width / 2.0, -child!.size.height / 2.0);
    } else {
    
    
      size = constraints.smallest;
    }
  }

  final LayerHandle<TransformLayer> _transformLayer =
  LayerHandle<TransformLayer>();

  void _paintChild(PaintingContext context, Offset offset) {
    
    
    print("paint child");
    context.paintChild(child!, offset);
  }


  
  void paint(PaintingContext context, Offset offset) {
    
    
    if (child != null) {
    
    
     _transformLayer.layer = context.pushTransform(
        needsCompositing, // pipelineOwner.flushCompositingBits(); 执行后这个值就能确定
        offset,
        _paintTransform!,
        _paintChild,
        oldLayer: _transformLayer.layer,
      );
    } else {
    
    
      _transformLayer.layer = null;
    }
  }


  
  void dispose() {
    
    
    _transformLayer.layer = null;
    super.dispose();
  }

  
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    
    
    if (_paintTransform != null) transform.multiply(_paintTransform!);
    super.applyPaintTransform(child, transform);
  }

}

是不是简洁清晰了很多!

flushCompositingBits 存在的意义

现在,我们思考一下引入 flushCompositingBits 的根本原因是什么?假如我们在变换类容器中始终采用合成 layer 的方式来对子树应用变换效果,也就是说不再使用 canvas 进行变换,这样的话 flushCompositingBits 也就没必要存在了,为什么一定要 flushCompositingBits 呢?根本原因就是:如果在变换类组件中一刀切的使用合成 layer 方式的话,每遇到一个变换类组件则至少会再创建一个 layer,这样的话,最终 layer 树上的layer数量就会变多。我们之前说过对子树应用的变换效果既能通过 Canvas 实现也能通过容器类Layer实现时,建议使用Canvas 。这是因为每新建一个 layer 都会有额外的开销,所以我们只应该在无法通过 Canvas 来实现子树变化效果时再通过Layer 合成的方式来实现。综上,我们可以发现引入 flushCompositingBits 的根本原因其实是为了减少 layer的数量。

另外,flushCompositingBits 的执行过程只是做标记,并没有进行层的合成,真正的合成是在绘制时(组件的 paint 方法中)。

总结

  1. 只有组件树中有变换类容器时,才有可能需要重新合成 layer;如果没有变换类组件,则不需要。

  2. 当变换类容器的后代节点会向 layer 树中添加新的绘制类 layer 时,则变换类组件中就需要合成 layer

  3. 引入 flushCompositingBits 的根本原因是为了减少 layer 的数量。

绘制流程总结

下面可以对 Flutter 的绘制流程做一个简要总结:

在这里插入图片描述

Flutter 渲染管道流程总结

下图是对 Flutter 渲染管道流程的概要总结:

在这里插入图片描述

在图5-16中,Vsync信号在到达Engine后,首先完成动画的刷新,其次在Engine中发起Dart VM中微任务的处理,最后回到Framework中,开始渲染管道的核心工作,主要包括Build、Layout、Paint、Composition、Rasterize这5个阶段。

  • Build阶段,将基于Widget Tree,在Element Tree(本质是BuildOwner)的驱动下,完成Render Tree原始数据的更新;
  • Layout阶段,Render Tree将在PipelineOwner的驱动下完成大小(Size)和偏移(Offset)等关键布局数据的计算;
  • Paint阶段,Render Tree将基于PaintingContext遍历每个节点,更新Framework中的Layer Tree
  • Composition阶段,Engine将以Framework阶段生成的Layer Tree为输入,合成最终的渲染数据Scene,并提交到pipeline
  • Rasterize阶段,Rasterizer将从pipeline中取出待渲染数据,最终绘制在目标Surface上,并显示给用户。

参考:

猜你喜欢

转载自blog.csdn.net/lyabc123456/article/details/130951747