深入学习Flutter的运行机制
我正在参加「掘金·启航计划」
main入口启动
- Flutter的主入口在"lib/main.dart"的
main()函数中。在Flutter应用中,main()函数最简单的实现如下:void main() { runApp(MyApp()); } - 可以看到
main()函数只调用了一个runApp()方法,runApp()方法中都做了什么:void runApp(Widget app) { //初始化操作 WidgetsFlutterBinding.ensureInitialized() //页面渲染 ..attachRootWidget(app) ..scheduleWarmUpFrame(); }- 参数
app是一个Widget,是Flutter应用启动后要展示的第一个Widget。 WidgetsFlutterBinding正是绑定widget 框架和Flutter engine的桥梁。
- 参数
ensureInitialized()方法class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { static WidgetsBinding ensureInitialized() { if (WidgetsBinding.instance == null) WidgetsFlutterBinding(); return WidgetsBinding.instance; } }- 可以看到
WidgetsFlutterBinding继承自BindingBase并混入了很多Binding,在介绍Binding之前先介绍一下Window,Window的官方解释:The most basic interface to the host operating system's user interface.
- 可以看到
Window正是Flutter Framework连接宿主操作系统的接口。看一下Window类的部分定义:class Window { // 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。 // DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5 double get devicePixelRatio => _devicePixelRatio; // 绘制回调 VoidCallback get onDrawFrame => _onDrawFrame; // 发送平台消息 void sendPlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) ; ... //其它属性及回调 }Window类包含了当前设备和系统的一些信息以及Flutter Engine的一些回调。现在回来看看WidgetsFlutterBinding混入的各种Binding。通过查看这些 Binding的源码,可以发现这些Binding中基本都是监听并处理Window对象的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。可以看到WidgetsFlutterBinding正是粘连Flutter engine与上层Framework的“胶水”。
- 看看
attachToRenderTree的源码实现:RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { if (element == null) { ...// 代码处理 } else { ...// 代码处理 } return element; } - 该方法负责创建根element,即
RenderObjectToWidgetElement,并且将element与widget 进行关联,即创建出 widget树对应的element树。- 如果element 已经创建过了,则将根element 中关联的widget 设为新的,由此可以看出element 只会创建一次,后面会进行复用。那么
BuildOwner是什么呢?其实他就是widget framework的管理类,它跟踪哪些widget需要重新构建。
- 如果element 已经创建过了,则将根element 中关联的widget 设为新的,由此可以看出element 只会创建一次,后面会进行复用。那么
页面渲染
- 回到
runApp的实现中,当调用完attachRootWidget后,最后一行会调用WidgetsFlutterBinding实例的scheduleWarmUpFrame()方法,该方法的实现在SchedulerBinding中,它被调用后会立即进行一次绘制(而不是等待"vsync"信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前Flutter将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。 - 下面是
scheduleWarmUpFrame()方法的部分实现(省略了无关代码):void scheduleWarmUpFrame() { Timer.run(() { handleBeginFrame(null); }); Timer.run(() { handleDrawFrame(); resetEpoch(); }); // 锁定事件 lockEvents(() async { await endOfFrame; Timeline.finishSync(); }); ... }- 可以看到该方法中主要调用了
handleBeginFrame()和handleDrawFrame()两个方法,在看这两个方法之前我们首先了解一下Frame 和 FrameCallback 的概念: - Frame: 一次绘制过程,我们称其为一帧。Flutter engine受显示器垂直同步信号"VSync"的驱使不断的触发绘制。我们之前说的Flutter可以实现60fps(Frame Per-Second),就是指一秒钟可以触发60次重绘,FPS值越大,界面就越流畅。
- FrameCallback:
SchedulerBinding类中有三个FrameCallback回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行:-
transientCallbacks:用于存放一些临时回调,一般存放动画回调。可以通过SchedulerBinding.instance.scheduleFrameCallback添加回调。
-
persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。
-
postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由SchedulerBinding.instance.addPostFrameCallback()注册,注意,不要在此类回调中再触发新的Frame,这可以会导致循环刷新。
-
- 自行查看
handleBeginFrame()和handleDrawFrame()两个方法的源码,可以发现前者主要是执行了transientCallbacks队列,而后者执行了persistentCallbacks和postFrameCallbacks队列。
- 可以看到该方法中主要调用了
页面绘制
- 渲染和绘制逻辑在
RendererBinding中实现,查看其源码,发现在其initInstances()方法中有如下代码:void initInstances() { ... //省略无关代码 //监听Window对象的事件 ui.window ..onMetricsChanged = handleMetricsChanged ..onTextScaleFactorChanged = handleTextScaleFactorChanged ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged ..onSemanticsAction = _handleSemanticsAction; //添加PersistentFrameCallback addPersistentFrameCallback(_handlePersistentFrameCallback); }- 看最后一行,通过
addPersistentFrameCallback向persistentCallbacks队列添加了一个回调_handlePersistentFrameCallback:
void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); } - 看最后一行,通过
- 该方法直接调用了
RendererBinding的drawFrame()方法
flushLayout()
- 代码如下所示
void flushLayout() { ... while (_nodesNeedingLayout.isNotEmpty) { final List<RenderObject> dirtyNodes = _nodesNeedingLayout; _nodesNeedingLayout = <RenderObject>[]; for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) { if (node._needsLayout && node.owner == this) node._layoutWithoutResize(); } } } } - 源码很简单,该方法主要任务是更新了所有被标记为“dirty”的
RenderObject的布局信息。主要的动作发生在node._layoutWithoutResize()方法中,该方法中会调用performLayout()进行重新布局。
flushCompositingBits()
- 代码如下所示
void flushCompositingBits() { _nodesNeedingCompositingBitsUpdate.sort( (RenderObject a, RenderObject b) => a.depth - b.depth ); for (RenderObject node in _nodesNeedingCompositingBitsUpdate) { if (node._needsCompositingBitsUpdate && node.owner == this) node._updateCompositingBits(); //更新RenderObject.needsCompositing属性值 } _nodesNeedingCompositingBitsUpdate.clear(); } - 检查
RenderObject是否需要重绘,然后更新RenderObject.needsCompositing属性,如果该属性值被标记为true则需要重绘。
flushPaint()
- 代码如下所示
void flushPaint() { ... try { final List<RenderObject> dirtyNodes = _nodesNeedingPaint; _nodesNeedingPaint = <RenderObject>[]; // 反向遍历需要重绘的RenderObject for (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(); } } } } }- 该方法进行了最终的绘制,可以看出它不是重绘了所有
RenderObject,而是只重绘了需要重绘的RenderObject。真正的绘制是通过PaintingContext.repaintCompositedChild()来绘制的,该方法最终会调用Flutter engine提供的Canvas API来完成绘制。
- 该方法进行了最终的绘制,可以看出它不是重绘了所有
compositeFrame()
- 代码如下所示
void compositeFrame() { ... try { final ui.SceneBuilder builder = ui.SceneBuilder(); final ui.Scene scene = layer.buildScene(builder); if (automaticSystemUiAdjustment) _updateSystemChrome(); ui.window.render(scene); //调用Flutter engine的渲染API scene.dispose(); } finally { Timeline.finishSync(); } } - 这个方法中有一个
Scene对象,Scene对象是一个数据结构,保存最终渲染后的像素信息。- 这个方法将Canvas画好的
Scene传给window.render()方法,该方法会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上。
- 这个方法将Canvas画好的
最后
- 需要注意:由于
RendererBinding只是一个mixin,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重写该方法,查看WidgetsBinding的drawFrame()方法源码:@override void drawFrame() { ...//省略无关代码 try { if (renderViewElement != null) buildOwner.buildScope(renderViewElement); super.drawFrame(); //调用RendererBinding的drawFrame()方法 buildOwner.finalizeTree(); } } - 发现在调用
RendererBinding.drawFrame()方法前会调用buildOwner.buildScope()(非首次绘制),该方法会将被标记为“dirty” 的 element 进行rebuild()。
转载自:https://juejin.cn/post/7249160565431222333