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;
}
}
- 重新构建 widget 树:如果 dirtyElements 列表不为空,则遍历该列表,调用每一个element的rebuild方法重新构建新的widget(树),由于新的widget(树)使用新的状态构建,所以可能导致widget布局信息(占用的空间和位置)发生变化,如果发生变化,则会调用其renderObject的markNeedsLayout方法,该方法会从当前节点向父级查找,直到找到一个relayoutBoundary的节点,然后会将它添加到一个全局的nodesNeedingLayout列表中;如果直到根节点也没有找到relayoutBoundary,则将根节点添加到nodesNeedingLayout列表中。
- 更新布局:遍历nodesNeedingLayout数组,对每一个renderObject重新布局(调用其layout方法),确定新的大小和偏移。layout方法中会调用markNeedsPaint(),该方法和 markNeedsLayout 方法功能类似,也会从当前节点向父级查找,直到找到一个isRepaintBoundary属性为true的父节点,然后将它添加到一个全局的nodesNeedingPaint列表中;由于根节点(RenderView)的 isRepaintBoundary 为 true,所以必会找到一个。查找过程结束后会调用 buildOwner.requestVisualUpdate 方法,该方法最终会调用scheduleFrame(),该方法中会先判断是否已经请求过新的frame,如果没有则请求一个新的frame。
- 更新合成信息:先忽略,我们在14.8节专门介绍。
- 更新绘制:遍历nodesNeedingPaint列表,调用每一个节点的paint方法进行重绘,绘制过程会生成Layer。需要说明一下,flutter中绘制结果是保存在Layer中的,也就是说只要Layer不释放,那么绘制的结果就会被缓存,因此,Layer可以跨frame来缓存绘制结果,避免不必要的重绘开销。Flutter框架绘制过程中,遇到isRepaintBoundary 为 true 的节点时,才会生成一个新的Layer。可见Layer和 renderObject 不是一一对应关系,父子节点可以共享,这个我们会在随后的一个试验中来验证。当然,如果是自定义组件,我们可以在renderObject中手动添加任意多个 Layer,这通常用于只需一次绘制而随后不会发生变化的绘制元素的缓存场景,这个随后我们也会通过一个例子来演示。
- 上屏:绘制完成后,我们得到的是一棵Layer树,最后我们需要将Layer树中的绘制信息在屏幕上显示。我们知道Flutter是自实现的渲染引擎,因此,我们需要将绘制信息提交给Flutter engine,而
renderView.compositeFrame正是完成了这个使命。
以上,便是setState被调用到UI更的大概更新过程,实际的流程会更复杂一些,比如在build过程中是不允许再调用setState的,框架需要做一些检查。又比如在frame中会涉及到动画的调度、在上屏时会将所有的Layer添加到场景(Scene)对象后,再渲染Scene。