Flutter Internals 时序图

2,198 阅读5分钟

【翻译】Flutter internals 中,我们了解了 Flutter Framework 框架是如何执行的。在本文中,我们将绘制出其中核心的流程的时序图,来加深对Flutter框架的理解。

Binding初始化

App 启动的时候,Flutter Engine 框架会调用 Flutter Framework 中的 main() 方法,而 main() 方法中首先做的事情就是初始化 binding 。

  1. Flutter Engine 框架调用 main() 方法,
  2. Flutter Framework 框架调用 runapp(Widget) 全局方法,传入我们自己的 widget(比如HomeWidget),
  3. 依次执行各个Binding的初始化方法,首先执行的是 GestureBinding ,
  4. GestureBinding 注册手势相关的回调,
  5. SchedulerBinding 注册帧率相关的回调,
  6. ServicesBinding 注册平台通信相关的回调,
  7. PaintingBinding 初始化ImageCache,
  8. SemanticsBinding 从 Window 获取辅助信息来初始化,
  9. RendererBinding 初始化 PipelineOwner,注册多个回调,初始化 RenderView,初始化 _persistentCallbacks ,
  10. WidgetsBinding 初始化 BuildOwner,注册多个回调,
  11. WidgetsFlutterBinding 创建了一个 widget ,这个 widget 把 renderView 作为 RenderObject,把 rootWidget(比如HomeWidget)作为 child,然后 inflate 出 根element ,让 WidgetBinding 的实例持有。
  12. WidgetsFlutterBinding 调用 handleBeginFrame, handleDrawFrame 渲染出第一帧

至此,binding的初始化就结束了,整个初始化过程完成这几件事情:

  1. binding 持有了一个 rootWidget , 这个 rootWidget inflate 了一个 rootElement ,持有 rootRenderObject(renderView) ,指定客户(我们)传入的 widget(比如HomeWidget) 作为 child 。有了这个基础,当 build() 的时候, "widget tree", element tree, renderObject tree 就形成了。
  2. 注册了多个回调,当 Engine 接收到硬件消息时,会通过这些回调通知 Framework ,比如触摸信息,
  3. 主动调用渲染相关接口,渲染出第一帧

画面更新

Ticker

Ticker 是 Flutter 中与动画密切相关的类,Ticker 的时序图也就是动画执行以及页面刷新的时序图。

  1. ticker 执行 start(),然后执行 scheduleTick()
  2. scheduleTick() 中会调用 SchedulerBinding 执行 scheduleFrameCallback(),然后让 window 执行 scheduleFrame() ,并将 _tick 注册为回调,
  3. 一段时间之后,window执行 _handleDrawFrame() 时,_tick 会执行,
  4. _tick 会在执行 ticker 初始化时传入的 _onTick()
  5. _onTick() 中可以执行修改 UI 的代码,比如平移等,然后执行完 _onTick() 之后 _tick 会再次调用 scheduleTick() ,来让 Engine 在下一帧时通知 ticker ,以便它能继续完成后续的动画。

State

请求页面更新

State 是我们最常用的类,因为当我们希望一个 Widget 能够变化的话,我们会使用 StatefulWidget 以及相应的 State 。下面的时序图展示了它刷新页面的逻辑。

  1. 执行setState() ,
  2. 调用对应的 element 的 markNeedsBuild() ,
  3. 该 element 被标记为 dirty,然后调用 buildOwner.scheduleBuildFor(this) ,将该 element 保存在 buildOwner 中,
  4. buildOwner 调用 widgetBinding 的 scheduleFrame() ,
  5. 最终调用window.scheduleFrame() ,

由此可以看出,我们使用setState()的时候,只做了这几件事:

  1. 标记 element 为 dirty ,并把它保存起来
  2. 请求页面更新

处理页面更新

setState() 之后,我们发起了一个页面更新的请求,在下一帧的时候,系统会要求 Framework 提供数据以供这一帧的渲染。

  1. window 调用 widgetsBinding 的 _handleDrawFrame() ,
  2. widgetsBinding 调用 buildOwner.buildScope() ,
  3. buildOwner 持有 _dirtyElements ,遍历 _dirtyElements 并执行 element.rebuild() ,
  4. element 执行performRebuild(),对于 statefulElement 而言,会执行 _state.didChangeDependencies() ,随后执行super.performRebuild() ,也就是 ComponentElement 的 performRebuild() 方法,
  5. ComponentElement 的 performRebuild() 里面会执行 build() ,然后 updateChild(_child, newWidget, slot) ,
  6. 至此就完成了所有 _dirtyElement(以及其child/children)的构建,element树 完成了更新,
  7. 紧接着就是渲染流程,分别执行 pipelineOwner 的 flushLayout、flushCompositingBits、flushPaint 方法,
  8. 最后调用 renderView.compositeFrame() 将生成的图像数据发送给 Engine ,
  9. 所有这一起完成之后,buildOwner 会执行finalizeTree ,将之前过程中对 element 树的修改实现。

这个过程包含相对复杂,其中 layout, paint 在这里都只是一笔带过, build() 方法也只介绍了 ComponnetElement 的这一种情况。

flushLayout

flushLayout 的任务就是算出 offset 和 size ,以便后面进行渲染。

  1. pipelineOwner 会遍历 _nodesNeedingLayout ,每一个 node 都是 renderObject,执行 node._layoutWithoutResize() ,
  2. 执行 renderObject 的 performLayout() ,
  3. 假设从 root renderObject 开始渲染,那么就是执行 renderView.performLayout() ,
  4. renderObject 中一般是先调用 child.layout(),然后根据 child 的 size 计算自己的 size ,
  5. layout() 方法中,会判断自己是否是 relayoutBoundary、是否 sizeByParent,然后执行 performLayout() ,执行完成之后获得自己的 size ,
  6. 如此循环,最终每个renderObject都知道自己的size ,
  7. 根据拿到的 size , parent renderObject 指定 child renderObject 的 offset ,layout 流程结束。

flushPaint

flushPaint 的任务就是生成 layer tree ,在后续的流程中 Engine 会用这个 layer tree 进行真正的渲染。

  1. PipelineOwner 遍历 _nodesNeedingPaint ,每一个 node 都是renderObject,执行 PaintingContext.repaintCompositedChild(node) ,执行的是 PaintingContext 的类方法,可以理解为一个全局方法,
  2. 然后用 renderObject 的 layer 实例化一个paintingContext,再调用 renderObject._paintWithContext ,
  3. 接着就是执行 renderObject 的 paint() 方法,
  4. 我们的 root renderObject(RenderView),它的 paint() 方法中,执行了context.paintChild ,
  5. paintChild 是 PaintContext 的实例方法,里面分为两种情况,
  6. 如果 childRenderObject 的 repaintBoundary 为 true,并且 _needPaint 的话,那么这个 childRenderObject 及其子类就自己绘制,也就是执行 repaintCompositedChild ,在绘制结束之后,执行 appendLayer ,将绘制好的 layer 添加到当前 layer 中,
  7. 如果 childRenderObject repaintBoundary 为 false,也就是说可以跟它的 parentRenderObject 画在一起,那么直接绘制到当前的 layer 中即可,
  8. 在 renderObject 的子类中,重写 paint 方法,并且调用 context.paintChild() ,即可遍历整个 renderObject 树,最终生成 layer tree 供后续渲染使用。

小结

本文基于 Flutter Framework 的源码,绘制了 binding 初始化和屏幕更新的时序图,希望能够帮助读者直接更直观地了解我们的 App 在初始化和屏幕更新的时候,都做了哪些事情。我们的 element tree, renderObject tree 以及 layer tree 是如何生成的。