在 【翻译】Flutter internals 中,我们了解了 Flutter Framework 框架是如何执行的。在本文中,我们将绘制出其中核心的流程的时序图,来加深对Flutter框架的理解。
Binding初始化
App 启动的时候,Flutter Engine 框架会调用 Flutter Framework 中的 main() 方法,而 main() 方法中首先做的事情就是初始化 binding 。
- Flutter Engine 框架调用 main() 方法,
- Flutter Framework 框架调用 runapp(Widget) 全局方法,传入我们自己的 widget(比如HomeWidget),
- 依次执行各个Binding的初始化方法,首先执行的是 GestureBinding ,
- GestureBinding 注册手势相关的回调,
- SchedulerBinding 注册帧率相关的回调,
- ServicesBinding 注册平台通信相关的回调,
- PaintingBinding 初始化ImageCache,
- SemanticsBinding 从 Window 获取辅助信息来初始化,
- RendererBinding 初始化 PipelineOwner,注册多个回调,初始化 RenderView,初始化 _persistentCallbacks ,
- WidgetsBinding 初始化 BuildOwner,注册多个回调,
- WidgetsFlutterBinding 创建了一个 widget ,这个 widget 把 renderView 作为 RenderObject,把 rootWidget(比如HomeWidget)作为 child,然后 inflate 出 根element ,让 WidgetBinding 的实例持有。
- WidgetsFlutterBinding 调用 handleBeginFrame, handleDrawFrame 渲染出第一帧
至此,binding的初始化就结束了,整个初始化过程完成这几件事情:
- binding 持有了一个 rootWidget , 这个 rootWidget inflate 了一个 rootElement ,持有 rootRenderObject(renderView) ,指定客户(我们)传入的 widget(比如HomeWidget) 作为 child 。有了这个基础,当 build() 的时候, "widget tree", element tree, renderObject tree 就形成了。
- 注册了多个回调,当 Engine 接收到硬件消息时,会通过这些回调通知 Framework ,比如触摸信息,
- 主动调用渲染相关接口,渲染出第一帧
画面更新
Ticker
Ticker 是 Flutter 中与动画密切相关的类,Ticker 的时序图也就是动画执行以及页面刷新的时序图。
- ticker 执行 start(),然后执行 scheduleTick()
- scheduleTick() 中会调用 SchedulerBinding 执行 scheduleFrameCallback(),然后让 window 执行 scheduleFrame() ,并将 _tick 注册为回调,
- 一段时间之后,window执行 _handleDrawFrame() 时,_tick 会执行,
- _tick 会在执行 ticker 初始化时传入的 _onTick()
- _onTick() 中可以执行修改 UI 的代码,比如平移等,然后执行完 _onTick() 之后 _tick 会再次调用 scheduleTick() ,来让 Engine 在下一帧时通知 ticker ,以便它能继续完成后续的动画。
State
请求页面更新
State 是我们最常用的类,因为当我们希望一个 Widget 能够变化的话,我们会使用 StatefulWidget 以及相应的 State 。下面的时序图展示了它刷新页面的逻辑。
- 执行setState() ,
- 调用对应的 element 的 markNeedsBuild() ,
- 该 element 被标记为 dirty,然后调用 buildOwner.scheduleBuildFor(this) ,将该 element 保存在 buildOwner 中,
- buildOwner 调用 widgetBinding 的 scheduleFrame() ,
- 最终调用window.scheduleFrame() ,
由此可以看出,我们使用setState()的时候,只做了这几件事:
- 标记 element 为 dirty ,并把它保存起来
- 请求页面更新
处理页面更新
setState() 之后,我们发起了一个页面更新的请求,在下一帧的时候,系统会要求 Framework 提供数据以供这一帧的渲染。
- window 调用 widgetsBinding 的 _handleDrawFrame() ,
- widgetsBinding 调用 buildOwner.buildScope() ,
- buildOwner 持有 _dirtyElements ,遍历 _dirtyElements 并执行 element.rebuild() ,
- element 执行performRebuild(),对于 statefulElement 而言,会执行 _state.didChangeDependencies() ,随后执行super.performRebuild() ,也就是 ComponentElement 的 performRebuild() 方法,
- ComponentElement 的 performRebuild() 里面会执行 build() ,然后 updateChild(_child, newWidget, slot) ,
- 至此就完成了所有 _dirtyElement(以及其child/children)的构建,element树 完成了更新,
- 紧接着就是渲染流程,分别执行 pipelineOwner 的 flushLayout、flushCompositingBits、flushPaint 方法,
- 最后调用 renderView.compositeFrame() 将生成的图像数据发送给 Engine ,
- 所有这一起完成之后,buildOwner 会执行finalizeTree ,将之前过程中对 element 树的修改实现。
这个过程包含相对复杂,其中 layout, paint 在这里都只是一笔带过, build() 方法也只介绍了 ComponnetElement 的这一种情况。
flushLayout
flushLayout 的任务就是算出 offset 和 size ,以便后面进行渲染。
- pipelineOwner 会遍历 _nodesNeedingLayout ,每一个 node 都是 renderObject,执行 node._layoutWithoutResize() ,
- 执行 renderObject 的 performLayout() ,
- 假设从 root renderObject 开始渲染,那么就是执行 renderView.performLayout() ,
- renderObject 中一般是先调用 child.layout(),然后根据 child 的 size 计算自己的 size ,
- layout() 方法中,会判断自己是否是 relayoutBoundary、是否 sizeByParent,然后执行 performLayout() ,执行完成之后获得自己的 size ,
- 如此循环,最终每个renderObject都知道自己的size ,
- 根据拿到的 size , parent renderObject 指定 child renderObject 的 offset ,layout 流程结束。
flushPaint
flushPaint 的任务就是生成 layer tree ,在后续的流程中 Engine 会用这个 layer tree 进行真正的渲染。
- PipelineOwner 遍历 _nodesNeedingPaint ,每一个 node 都是renderObject,执行 PaintingContext.repaintCompositedChild(node) ,执行的是 PaintingContext 的类方法,可以理解为一个全局方法,
- 然后用 renderObject 的 layer 实例化一个paintingContext,再调用 renderObject._paintWithContext ,
- 接着就是执行 renderObject 的 paint() 方法,
- 我们的 root renderObject(RenderView),它的 paint() 方法中,执行了context.paintChild ,
- paintChild 是 PaintContext 的实例方法,里面分为两种情况,
- 如果 childRenderObject 的 repaintBoundary 为 true,并且 _needPaint 的话,那么这个 childRenderObject 及其子类就自己绘制,也就是执行 repaintCompositedChild ,在绘制结束之后,执行 appendLayer ,将绘制好的 layer 添加到当前 layer 中,
- 如果 childRenderObject repaintBoundary 为 false,也就是说可以跟它的 parentRenderObject 画在一起,那么直接绘制到当前的 layer 中即可,
- 在 renderObject 的子类中,重写 paint 方法,并且调用 context.paintChild() ,即可遍历整个 renderObject 树,最终生成 layer tree 供后续渲染使用。
小结
本文基于 Flutter Framework 的源码,绘制了 binding 初始化和屏幕更新的时序图,希望能够帮助读者直接更直观地了解我们的 App 在初始化和屏幕更新的时候,都做了哪些事情。我们的 element tree, renderObject tree 以及 layer tree 是如何生成的。