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