Flutter 在 Framework 上打设计大量借鉴 React,做到通过声明的方式编写 UI 代码,让开发者围绕 Widget 进行开发。但仅仅这样是不够的,我们知道 Flutter 相比于 React 还多了关于测量、布局以及绘制的逻辑,而这些都是在前端开发中很难触碰到的,既然 Flutter 提供了这样的机会,理解其中的原理就显得尤为必要了。 这篇文章主要分析 Flutter UI 更新背后的逻辑。
入口 在 这篇文章 中分析了 runApp
的启动流程,也知道了这里是触发界面绘制的入口之一。还有一个比较常见的触发界面刷新的入口是 State#setState
方法,这里主要就分析 setState
内部逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void setState(VoidCallback fn) { final dynamic result = fn() as dynamic ; if (result is Future) { throw FlutterError("" ); } _element.markNeedsBuild(); } void markNeedsBuild() { if (!_active) return ; if (dirty) return ; _dirty = true ; owner.scheduleBuildFor(this ); }
setState
的工作就是更新状态位,同时向 BuildOwner
说明自己需要被重建。这个 BuildOwner
控制视图树的更新,封装了 diff 算法。
BuildOwner setState
在简单状态位后就将剩下的工作交给了 BuildOwner
,看下代码:
1 2 3 4 5 6 7 8 9 10 11 12 void scheduleBuildFor(Element element) { if (element._inDirtyList) { _dirtyElementsNeedsResorting = true ; return ; } if (!_scheduledFlushDirtyElements && onBuildScheduled != null ) { _scheduledFlushDirtyElements = true ; onBuildScheduled(); } _dirtyElements.add(element); element._inDirtyList = true ; }
这里逻辑也非常简单的,将 element
添加到 _dirtyelements
中以等待重建,然后调用 onBuildScheduled
,那这个 onBuildScheduled
属性又是在什么时候赋值的呢?WidgetsBinding
在初始化时对 BuildOwner#onBuildScheduled
进行了赋值:
1 2 3 4 5 6 7 mixin WidgetsBinding { void initInstances() { buildOwner.onBuildScheduled = _handleBuildScheduled; } }
所以调用 BuildOwner#onBuildScheduled
实际是调用的 WidgetsBinding#_handleBuildScheduled
。接着函数调用往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void _handleBuildScheduled() { ensureVisualUpdate(); } void ensureVisualUpdate() { switch (schedulerPhase) { case SchedulerPhase.idle: case SchedulerPhase.postFrameCallbacks: scheduleFrame(); return ; case SchedulerPhase.transientCallbacks: case SchedulerPhase.midFrameMicrotasks: case SchedulerPhase.persistentCallbacks: return ; } } void scheduleFrame() { if (_hasScheduledFrame || !_framesEnabled) return ; ui.window .scheduleFrame(); _hasScheduledFrame = true ; }
这里最终会调用到 ui.window.scheduleFrame
,事实上调用流程到这里也就结束,不过最重要的也是这里,看下它的注释:
1 2 3 void scheduleFrame() native 'Window_scheduleFrame' ;
这是一个 native 方法,大概意思是 通知引擎 在合适的时候驱动界面刷新,具体是通过 onBeginFrame
和 onDrawFrame
完成的。
目前 Flutter/Dart 在调用 C++ 代码方面还没有官方的文档,但随着迭代后期肯定会有相应的工具出现
SchedulerBinding 上一节知道了界面的更新最后会通过 ui.window.onBeginFrame
和 ui.window.onDrawFrame
触发,而这两个函数的赋值是在 SchedulerBinding
的初始化函数中完成的:
1 2 3 4 5 6 7 8 9 mixin SchedulerBinding { @override void initInstances() { ui.window .onBeginFrame = _handleBeginFrame; ui.window .onDrawFrame = _handleDrawFrame; } }
接着往下看会发现最终调用的是 handleBeginFrame
和 handleDrawFrame
,这样就又回到了这篇文章 ,即会调用 WidgetsBinding#drawFrame
方法
至于为什么是 WidgetsBinding#drawFrame
需要理解 Dart 的 mixin
drawFrame 先看 WidgetsBinding
中的实现:
1 2 3 4 5 6 7 void drawFrame(){ buildOwner.buildScope(renderViewElement); super .drawFrame(); buildOwner.finalizeTree(); }
这里又回到了 BuildOwner
,看看 buildScope
方法里头在更新界面方面所做的工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void buildScope(Element context, [VoidCallback callback]) { if (callback == null && _dirtyElements.isEmpty) return ; try { _scheduledFlushDirtyElements = true ; if (callback != null ) { _dirtyElementsNeedsResorting = false ; callback(); } _dirtyElements.sort(Element ._sort); _dirtyElementsNeedsResorting = false ; int dirtyCount = _dirtyElements.length; int index = 0 ; while (index < dirtyCount) { _dirtyElements[index].rebuild(); index += 1 ; if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) { _dirtyElements.sort(Element ._sort); _dirtyElementsNeedsResorting = false ; dirtyCount = _dirtyElements.length; while (index > 0 && _dirtyElements[index - 1 ].dirty) { index -= 1 ; } } } } finally { for (Element element in _dirtyElements) { element._inDirtyList = false ; } _dirtyElements.clear(); _scheduledFlushDirtyElements = false ; _dirtyElementsNeedsResorting = null ; } }
在分析 RendererBinding#drawFrame
之前先分析 BuildOwner#finalizeTree
:
1 2 3 4 5 6 7 void finalizeTree() { lockState(() { _inactiveElements._unmountAll(); }); }
这里就是将 _inactiveElements
进行清理,也就是说使用 GlobalKeys
缓存的控件只能被下一帧使用,然后就会被清理。
Element#rebuild 上面如果点进 Element.rebuild
后发现代码实现和重建更新没啥关系,因为能够根据状态重建更新的只能是 容器组件 或 根节点 ,根节点的重建更新是由 runApp
触发的,容器组件的更新是由 setState
触发,所以下面看 ComponentElement
(它是 StatefulElement
的父类) 中的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void rebuild() { if (!_active || !_dirty) return ; performRebuild(); } void performRebuild() { Widget built = build(); _dirty = false ; _child = updateChild(_child, built, slot); } Element updateChild(Element child, Widget newWidget, dynamic newSlot) { if (newWidget == null ) { if (child != null ) deactivateChild(child); return null ; } if (child != null ) { if (child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } if (Widget.canUpdate(child.widget, newWidget)) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); return child; } deactivateChild(child); } return inflateWidget(newWidget, newSlot); }
大致的更新逻辑就是这样,简单总结下:
newWidget == null,即 widget.build() == null 时返回 null;如果 child == null 则删除子树;流程结束 child == null(一般是 runApp 触发),递归地重建子树;结束流程 child != null && widget 没变化;不做更新;结束流程 child != null && widget 发生变化可以更新;结束流程 child != null && widget 发生变化不可更新;卸载子树;递归重建子树;结束流程
源码中使用表格进行表述:
newWidget == null newWidget != null child == null Returns null. Returns new [Element]. child != null Old child is removed, returns null. Old child updated if possible, returns child or new [Element].
PipelineOwner 回到 PipelineOwner#drawFrame
:
1 2 3 4 5 6 7 void drawFrame() { pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); renderView.compositeFrame(); pipelineOwner.flushSemantics(); }
这里就没啥废话好讲了,直接看代码吧!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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(); } } } void _layoutWithoutResize() { performLayout(); markNeedsSemanticsUpdate(); _needsLayout = false ; markNeedsPaint(); } void markNeedsPaint() { if (_needsPaint) return ; _needsPaint = true ; if (isRepaintBoundary) { if (owner != null ) { owner._nodesNeedingPaint.add(this ); owner.requestVisualUpdate(); } } else if (parent is RenderObject) { final RenderObject parent = this .parent; parent.markNeedsPaint(); } else { if (owner != null ) owner.requestVisualUpdate(); } }
这里有个新的概念:重绘边界 , flushLayout
会尝试将布局边界添加到重绘列表中,如果没找到就会将根结点加入,也就是说设置布局边界可以避免全量重绘。同理,还有一个 布局边界 ,也能起到减少布局的开销。
简单总结 setState
会将 Element
设置位 dirty,然后通知引擎需要重新布局等,之后的操作和启动第一帧上屏就是一样的了。看到这里也能理解为什么第一帧上屏和常规的界面更新会有不一样的地方了,因为常规更新只会触发状态位更新而不会立即更新,直到下一次 VSync 信号到来之时才能开始刷新。
References https://juejin.im/post/5c0fc3cb5188251da07e09b3