Flutter 渲染管线(rendering pipeline)

271 阅读4分钟

一次绘制过程,称其为一帧(frame)

Flutter 可以实现60fps(Frame Per-Second)就是指一秒钟最多可以触发 60 次重绘,FPS 值越大,界面就越流畅

Flutter中 的 frame 概念并不等同于屏幕刷新帧(frame),因为Flutter UI 框架的 frame 并不是每次屏幕刷新都会触发,这是因为,如果 UI 在一段时间不变,那么每次屏幕刷新都重新走一遍渲染流程是不必要的,因此,Flutter 在第一帧渲染结束后会采取一种主动请求 frame 的方式来实现只有当UI可能会改变时才会重新走渲染流程。

  1. Flutter 在 window 上注册一个 onBeginFrame和一个 onDrawFrame 回调,在onDrawFrame 回调中最终会调用 drawFrame
  2. 当我们调用 window.scheduleFrame() 方法之后,Flutter引擎会在合适的时机(可以认为是在屏幕下一次刷新之前,具体取决于Flutter引擎的实现)来调用onBeginFrameonDrawFrame

可以看见,只有主动调用scheduleFrame(),才会执行 drawFrame。所以,在Flutter 中的提到 frame 时,如无特别说明,则是和 drawFrame() 的调用对应,而不是和屏幕的刷新频率对应

 Flutter 调度过程 SchedulerPhase

Flutter 应用执行过程简单来讲分为 idle 和 frame 两种状态,idle 状态代表没有 frame 处理,如果应用状态改变需要刷新 UI,则需要通过scheduleFrame()去请求新的 frame,当 frame 到来时,就进入了frame状态,整个Flutter应用生命周期就是在 idle 和 frame 两种状态间切换

1、frame 处理流程

当有新的 frame 到来时,具体处理过程就是依次执行四个任务队列:transientCallbacks、midFrameMicrotasks、persistentCallbacks、postFrameCallbacks,当四个任务队列执行完毕后当前 frame 结束。综上,Flutter 将整个生命周期分为五种状态,通过 SchedulerPhase 枚举类来表示它们:

enum SchedulerPhase {
  
  /// 空闲状态,并没有 frame 在处理。这种状态代表页面未发生变化,并不需要重新渲染。
  /// 如果页面发生变化,需要调用`scheduleFrame()`来请求 frame。
  /// 注意,空闲状态只是指没有 frame 在处理,通常微任务、定时器回调或者用户事件回调都
  /// 可能被执行,比如监听了tap事件,用户点击后我们 onTap 回调就是在idle阶段被执行的。
  idle,

  /// 执行”临时“回调任务,”临时“回调任务只能被执行一次,执行后会被移出”临时“任务队列。
  /// 典型的代表就是动画回调会在该阶段执行。
  transientCallbacks,

  /// 在执行临时任务时可能会产生一些新的微任务,比如在执行第一个临时任务时创建了一个
  /// Future,且这个 Future 在所有临时任务执行完毕前就已经 resolve 了,这中情况
  /// Future 的回调将在[midFrameMicrotasks]阶段执行
  midFrameMicrotasks,

  /// 执行一些持久的任务(每一个frame都要执行的任务),比如渲染管线(构建、布局、绘制)
  /// 就是在该任务队列中执行的.
  persistentCallbacks,

  /// 在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和
  /// 请求新的 frame。
  postFrameCallbacks,
}

渲染管线就是在 persistentCallbacks 中执行的.

当新的 frame 到来时,调用到 WidgetsBinding 的 drawFrame() 方法

@override
void drawFrame() {
 ...//省略无关代码
  try {
    buildOwner.buildScope(renderViewElement); // 先执行构建
    super.drawFrame(); //然后调用父类的 drawFrame 方法
  } 
}

实际上关键的代码就两行:先重新构建(build),然后再调用父类的 drawFrame 方法,将父类的 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
    ...
  }
}

可以看到主要做了5件事:

  1. 重新构建widget树。
  2. 更新布局。
  3. 更新“层合成”信息。
  4. 重绘。
  5. 上屏:将绘制的产物显示在屏幕上

上面的5步为 rendering pipeline,中文翻译为 “渲染流水线” 或 “渲染管线”。而渲染管线的这 5 个步骤的具体过程便是本章重点要介绍的。

2、setState 执行流

setState 调用后:

  1. 首先调用当前 element 的 markNeedsBuild 方法,将当前 element标记为 dirty 。
  2. 接着调用 scheduleBuildFor,将当前 element 添加到pipelineOwner的 dirtyElements 列表。
  3. 最后请求一个新的 frame,随后会绘制新的 frame:onBuildScheduled->ensureVisualUpdate->scheduleFrame() 。当新的 frame 到来时执行渲染管线
void drawFrame() {
  buildOwner!.buildScope(renderViewElement!); //重新构建widget树
  pipelineOwner.flushLayout(); // 更新布局
  pipelineOwner.flushCompositingBits(); //更新合成信息
  pipelineOwner.flushPaint(); // 更新绘制
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // 上屏,会将绘制出的bit数据发送给GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true;
  }
}

以上,便是setState被调用到UI更的大概更新过程,实际的流程会更复杂一些,比如在build过程中是不允许再调用setState的,框架需要做一些检查。又比如在frame中会涉及到动画的调度、在上屏时会将所有的Layer添加到场景(Scene)对象后,再渲染Scene