從setState開始,探索Flutter的檢視更新流程

RayC發表於2021-03-10

​ 如何重新整理Flutter應用的一個介面?被問到這個問題,相信很多人第一個想到的都是setState。沒錯,setState方法確實可以讓Statebuild方法重走,從而達到重新整理介面的效果。但是你有沒有想過,為什麼setState可以觸發rebuild呢?rebuild後檢視又是如何更新的呢?

​ 帶著這些疑問,有目標的開始探索原始碼吧!

1、setState做了什麼

State -> setState

[->flutter/src/widgets/framework.dart]

void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
     ///...
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
     ///...
    }
    return true;
  }());
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
     ///...
    return true;
  }());
  _element.markNeedsBuild();
}
複製程式碼

​ 點進setState方法我們會發現有一堆assert,最終會呼叫_element.markNeedsBuild()方法。

StatefulElement -> markNeedsBuild

[->flutter/src/widgets/framework.dart]

void markNeedsBuild() {
  assert(_debugLifecycleState != _ElementLifecycle.defunct);
  if (!_active)
    return;
  assert(owner != null);
  assert(_debugLifecycleState == _ElementLifecycle.active);
	///...
  if (dirty)
    return;
  _dirty = true;
  owner.scheduleBuildFor(this);
}
複製程式碼

StatefulElement本身沒有重寫markNeedsBuild方法,所以我們最終呼叫的還是Element中的markNeedsBuild方法。可以看到上面也是一堆斷言,最終判斷當前element是否已被標記為dirty,如果沒有則標記為dirty,然後呼叫owner.scheduleBuildFor(this)方法。

BuildOwner -> scheduleBuildFor

[->flutter/src/widgets/framework.dart]

void scheduleBuildFor(Element element) {
  assert(element != null);
  assert(element.owner == this);
  ///...
  if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
    _scheduledFlushDirtyElements = true;
    onBuildScheduled();
  }
  _dirtyElements.add(element);
  element._inDirtyList = true;
  ///...
}
複製程式碼

scheduleBuildFor方法首先會去呼叫onBuildScheduled方法,然後把element放入_dirtyElements中,並且標記為dirty。只是加入髒列表中的話並不會造成什麼實際效果,因此主動觸發重新整理的邏輯應該是在onBuildScheduled方法中。

/// Called on each build pass when the first buildable element is marked
/// dirty.
VoidCallback onBuildScheduled;
複製程式碼

onBuildScheduled是一個回撥,我們需要追蹤傳入的地方。

WidgetsBinding -> initInstances

[->flutter/src/widgets/binding.dart]

void initInstances() {
  super.initInstances();
  _instance = this;

  assert(() {
    _debugAddStackFilters();
    return true;
  }());

  // Initialization of [_buildOwner] has to be done after
  // [super.initInstances] is called, as it requires [ServicesBinding] to
  // properly setup the [defaultBinaryMessenger] instance.
  _buildOwner = BuildOwner();
  buildOwner.onBuildScheduled = _handleBuildScheduled;
  window.onLocaleChanged = handleLocaleChanged;
  window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
  SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
  FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
}
複製程式碼

​ 通過追蹤我們找到了BuildOwner被建立的地方,也就是WidgetsBinding的初始化方法。onBuildScheduled的呼叫最終會呼叫WidgetsBinding_handleBuildScheduled方法。

WidgetsBinding -> _handleBuildScheduled

[->flutter/src/widgets/binding.dart]

void _handleBuildScheduled() {
  // If we're in the process of building dirty elements, then changes
  // should not trigger a new frame.
 	///...
  ensureVisualUpdate();
}
複製程式碼

_handleBuildScheduled方法上面也是個很長的斷言,最終會呼叫ensureVisualUpdate方法。

SchedulerBinding -> ensureVisualUpdate

[->flutter/src/scheduler/binding.dart]

void ensureVisualUpdate() {
  switch (schedulerPhase) {
    case SchedulerPhase.idle:
    case SchedulerPhase.postFrameCallbacks:
      scheduleFrame();
      return;
    case SchedulerPhase.transientCallbacks:
    case SchedulerPhase.midFrameMicrotasks:
    case SchedulerPhase.persistentCallbacks:
      return;
  }
}
複製程式碼

​ 此方法會判斷當前的階段,如果處於空閒或者下一幀回撥狀態,則會執行scheduleFrame方法,否則什麼也不做。

SchedulerBinding -> scheduleFrame

[->flutter/src/scheduler/binding.dart]

void scheduleFrame() {
  if (_hasScheduledFrame || !framesEnabled)
    return;
  ///...
  ensureFrameCallbacksRegistered();
  window.scheduleFrame();
  _hasScheduledFrame = true;
}
複製程式碼

​ 此方法首先會呼叫ensureFrameCallbacksRegistered方法。然後呼叫window的scheduleFrame方法。

SchedulerBinding -> ensureFrameCallbacksRegistered

void ensureFrameCallbacksRegistered() {
  window.onBeginFrame ??= _handleBeginFrame;
  window.onDrawFrame ??= _handleDrawFrame;
}
複製程式碼

​ 此方法會註冊window下的onBeginFrameonDrawFrame回撥。

Window -> scheduleFrame

[->sky_engine/ui/window.dart]

/// Requests that, at the next appropriate opportunity, the [onBeginFrame]
/// and [onDrawFrame] callbacks be invoked.
///
/// See also:
///
///  * [SchedulerBinding], the Flutter framework class which manages the
///    scheduling of frames.
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
複製程式碼

​ 走到這裡,就陷入native層了。通過註釋我們可以看到,onBeginFrameonDrawFrame回撥將會被呼叫。

SchedulerBinding -> _handleBeginFrame

[->flutter/src/scheduler/binding.dart]

void _handleBeginFrame(Duration rawTimeStamp) {
  if (_warmUpFrame) {//如果當前幀已被處理 直接return
    assert(!_ignoreNextEngineDrawFrame);
    _ignoreNextEngineDrawFrame = true;//忽略後續的drawFrame
    return;
  }
  handleBeginFrame(rawTimeStamp);
}
複製程式碼

​ 如果當前幀還未被處理,則會呼叫handleBeginFrame方法

SchedulerBinding -> handleBeginFrame

void handleBeginFrame(Duration? rawTimeStamp) {
  Timeline.startSync('Frame', arguments: timelineArgumentsIndicatingLandmarkEvent);
  _firstRawTimeStampInEpoch ??= rawTimeStamp;
  //調整當前幀時間戳
  _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
  if (rawTimeStamp != null)
    _lastRawTimeStamp = rawTimeStamp;
  ///...

  assert(schedulerPhase == SchedulerPhase.idle);//只有當前進度為空閒才可往下走
  _hasScheduledFrame = false;
  try {
    // TRANSIENT FRAME CALLBACKS
    Timeline.startSync('Animate', arguments: timelineArgumentsIndicatingLandmarkEvent);
    _schedulerPhase = SchedulerPhase.transientCallbacks;//更新進度
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
    _transientCallbacks = <int, _FrameCallbackEntry>{};
    //回撥
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
      if (!_removedIds.contains(id))
        _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
    });
    _removedIds.clear();
  } finally {
    _schedulerPhase = SchedulerPhase.midFrameMicrotasks;//更新進度
  }
}
複製程式碼

​ 可以看到這個方法主要是用來進行一些調整,然後回撥當前的進度。

SchedulerBinding -> _handleDrawFrame

[->flutter/src/scheduler/binding.dart]

void _handleDrawFrame() {
  if (_ignoreNextEngineDrawFrame) {
    _ignoreNextEngineDrawFrame = false;
    return;
  }
  handleDrawFrame();
}
複製程式碼

​ 如果該幀沒被忽略,則會呼叫到handleDrawFrame中。

SchedulerBinding -> handleDrawFrame

void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  Timeline.finishSync(); // end the "Animate" phase
  try {
    // PERSISTENT FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    ///持久幀回撥
    for (final FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp!);

    // POST-FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();    //postFrameCallback被呼叫後會清除
    for (final FrameCallback callback in localPostFrameCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp!);
  } finally {
    _schedulerPhase = SchedulerPhase.idle;//更新狀態
    Timeline.finishSync(); // end the Frame
   ///...
    _currentFrameTimeStamp = null;
  }
}
複製程式碼

​ 可以看到此方法也是用來進行回撥的。

​ 至此,onBuildScheduled回撥執行完畢。element被加入了_dirtyElements中,setState方法也執行完成了。

小結

​ 看完上面的一大堆的呼叫,我們可以知道setState本質上是呼叫了elementmarkNeedsBuild方法。該方法會嘗試觸發幀的排程,然後把當前element加入到BuildOwner的髒列表中。在髒列表中的資料在一系列排程後,會被更新到螢幕上。

image-20210309144906941

2、如何重新整理一個StatelessWidget

​ 這個問題我之前在面試中被問過。當時我並沒有太深入閱讀Flutter的原始碼,因此當時我的回答是用外部重新整理。現在想想,回答的還是有點膚淺了。

​ 雖然StatelessWidget設計上是無狀態的,也沒有暴露任何重新整理的方法,但是我們想要去重新整理它也不是不可以的。上面分析了那麼多我們已經知道了,setState重新整理一個StatefulWidget的本質是呼叫elementmarkNeedsBuild方法來觸發更新,而StatelessWidget自身也是有Element的。因此我們只要呼叫了StatelessWidgetElementmarkNeedsBuild方法,就可以重新整理一個StatelessWidget

​ 程式碼驗證一下:

class RefreshStateless extends StatelessWidget {
  StatelessElement element;
  String text = '測試';

  @override
  Widget build(BuildContext context) {
    element = context;
    return GestureDetector(
      onTap: () {
        text = '我被重新整理啦';
        element.markNeedsBuild();
      },
      child: Container(
        child: Text(text),
      ),
    );
  }
}
複製程式碼

​ 在build方法中我們儲存了StatelessWidgetBuildContext也就是StatelessElement。最開始我們展示的是測試字樣。點選後我們把文字更新為我被重新整理啦,然後手動呼叫markNeedsBuild方法。

​ 一開始執行後顯示的使我們設定的初始值:

image-20210309110852221

​ 點選後會重新整理成如下:

image-20210309110935316

​ 可以看到重新整理成功了。只要我們改變了build方法下的描述內容,StatelessWidget也是可以被重新整理的,當然並不推薦這麼做。

3、髒列表是如何被更新的

​ 上文我們已經知道了setState的本質是將當前的element加入髒列表,髒列表中的資料在後續會被排程處理。現在我們來追蹤一下髒列表,看看它是如何被處理的。

BuildOwner -> buildScope

​ 首先在BuildOwner下我們發現髒列表會在buildScope方法下被處理,接著追蹤該方法的呼叫:

image-20210309123039076

WidgetsBinding -> drawFrame

void drawFrame() {
///...
try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();//call super
      buildOwner.finalizeTree();
    }
  ///...
}
複製程式碼

​ 由呼叫鏈我們追蹤到WidgetsBindingdrawFrame方法下呼叫了此方法,呼叫完成後會再呼叫父類的drawFrame方法。現在需要再看看WidgetsBindingdrawFrame方法是如何被呼叫的:

image-20210309123401803

​ 可以看到只在一處被呼叫。

RendererBinding -> _handlePersistentFrameCallback

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();//呼叫drawFrame
  _scheduleMouseTrackerUpdate();
}
複製程式碼

​ 繼續追蹤呼叫:

RendererBinding -> initInstances

void initInstances() {
  super.initInstances();
  _instance = this;
  _pipelineOwner = PipelineOwner(
    onNeedVisualUpdate: ensureVisualUpdate,
    onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
    onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
  );
  window
    ..onMetricsChanged = handleMetricsChanged
    ..onTextScaleFactorChanged = handleTextScaleFactorChanged
    ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
    ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
    ..onSemanticsAction = _handleSemanticsAction;
  initRenderView();
  _handleSemanticsEnabledChanged();
  assert(renderView != null);
  addPersistentFrameCallback(_handlePersistentFrameCallback);//持久回撥
  initMouseTracker();
}
複製程式碼

​ 我們可以看到該方法被註冊為一個持久的監聽,每當有幀更新的時候都會被呼叫。該回撥具體的回撥時機我們已經分析過了,在觸發了windowshceduleFrame回撥後,會被呼叫,具體回撥的實現SchedulerBInding 的 handleDrawFrame方法中。

​ 現在我們可以知道了髒列表更新的方法呼叫順序了:

image-20210309155450174

BuildOwner -> buildScope

void buildScope(Element context, [ VoidCallback callback ]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  ///... assert部分
  Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);//開始Build
  try {
    _scheduledFlushDirtyElements = true;
    if (callback != null) {
     	///...
      _dirtyElementsNeedsResorting = false;
      try {
        callback();
      } finally {
        ///... a
      }
    }
    _dirtyElements.sort(Element._sort);//按深度排序 自上而下開始構建
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      ///... assert
      try {
        _dirtyElements[index].rebuild();//呼叫 dirty element的rebuild方法
      } catch (e, stack) {
        /// ...error handle
      }
      index += 1;
      if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
        //build之後可能會有新的髒列表資料 在此進行處理
        _dirtyElements.sort(Element._sort);
        _dirtyElementsNeedsResorting = false;
        dirtyCount = _dirtyElements.length;//調整dirtyCount
        while (index > 0 && _dirtyElements[index - 1].dirty) {
          // It is possible for previously dirty but inactive widgets to move right in the list.
          // We therefore have to move the index left in the list to account for this.
          // We don't know how many could have moved. However, we do know that the only possible
          // change to the list is that nodes that were previously to the left of the index have
          // now moved to be to the right of the right-most cleaned node, and we do know that
          // all the clean nodes were to the left of the index. So we move the index left
          // until just after the right-most clean node.
          index -= 1;
        }
      }
    }
    ///...
  } finally {
    for (final Element element in _dirtyElements) {
      assert(element._inDirtyList);
      element._inDirtyList = false;
    }
    _dirtyElements.clear();
    _scheduledFlushDirtyElements = false;
    _dirtyElementsNeedsResorting = null;
    Timeline.finishSync();
   ///...
  }
  assert(_debugStateLockLevel >= 0);
}
複製程式碼

​ buildScope方法會將_dirtyElements列表資料重新排序,然後自上而下呼叫rebuild方法。

Element -> rebuild

void rebuild() {
  ///... assert 
  performRebuild();//實際執行build的地方
 ///... assert
}
複製程式碼

rebuild方法裡只是一堆判斷和斷言,最後把處理邏輯交給了performRebuild方法。不同的Element有不同的處理邏輯。這裡看一下StatefulElement的實現。

image-20210309171312116

StatefulElement -> performRebuild

void performRebuild() {
  if (_didChangeDependencies) {
    _state.didChangeDependencies();
    _didChangeDependencies = false;
  }
  super.performRebuild();
}
複製程式碼

​ 如果依賴發生了改變,會回撥state的didChangeDependencies,接著呼叫了父類的performRebuild方法。

ComponentElement -> performRebuild

void performRebuild() {
  if (!kReleaseMode && debugProfileBuildsEnabled)
    Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);

  assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
  Widget built;
  try {
    assert(() {
      _debugDoingBuild = true;
      return true;
    }());
    built = build();//呼叫build方法
    assert(() {
      _debugDoingBuild = false;
      return true;
    }());
    debugWidgetBuilderValue(widget, built);
  } catch (e, stack) {
    ///...
  } finally {
    // We delay marking the element as clean until after calling build() so
    // that attempts to markNeedsBuild() during build() will be ignored.
    _dirty = false;
    assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
  }
  try {
    _child = updateChild(_child, built, slot);//更新element
    assert(_child != null);
  } catch (e, stack) {
   ///...
  if (!kReleaseMode && debugProfileBuildsEnabled)
    Timeline.finishSync();
}
複製程式碼

​ 這裡我們可以看到build方法被呼叫,生成了新的Widget配置,接著通過updateChild方法來更新element。updateChild方法的實現繼承自Element

Element -> updateChild

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  Element newChild;
  if (child != null) {
    bool hasSameSuperclass = true;
    assert(() {
      final int oldElementClass = Element._debugConcreteSubtype(child);
      final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
      ///hot reload情況下可能會發生element型別改變
      hasSameSuperclass = oldElementClass == newWidgetClass;
      return true;
    }());
    if (hasSameSuperclass && child.widget == newWidget) {
      //element型別一致 配置一致
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
       //element型別一致 配置型別和key一致
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);//更新widget
      assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true;
      }());
      newChild = child;
    } else {
      deactivateChild(child);
      assert(child._parent == null);
      //建立新的element
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
     //建立新的element
    newChild = inflateWidget(newWidget, newSlot);
  }
	///...
  return newChild;
}
複製程式碼

​ 這段程式碼的邏輯並不複雜。如果newWidget為null,則element的配置沒有了,這時候需要把這個element從樹中移除。如果之前的child為空,則載入newWidget的配置,生成一個element返回。否則會判斷child能否更新。只有不能更新的情況下才會建立一個新的Element。

StatefulElement -> update

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  assert(widget == newWidget);
  final StatefulWidget oldWidget = _state._widget;
  _dirty = true;
  _state._widget = widget as StatefulWidget;
  try {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
    final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;//回撥didUpdateWidget
   ///...
  } finally {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  }
  rebuild();//rebuild
}
複製程式碼

​ StatefulElement在update方法中回撥了didUpdateWidget生命週期並馬上rebuild。

RenderBinding -> drawFrame

void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  if (sendFramesToEngine) {
    renderView.compositeFrame(); //實際渲染的地方
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true;
  }
}
複製程式碼

​ 終於走到這裡,這也是實際構建檢視的地方。pipelineOwner會進行佈局、混合、繪製等一系列的操作。最後通過RenderViewcompositeFrame實際的去進行渲染一幀。

BuildOwner -> finalizeTree

void finalizeTree() {
  Timeline.startSync('Finalize tree', arguments: timelineArgumentsIndicatingLandmarkEvent);
  try {
    lockState(() {
      //取消掛載不活動的element
      _inactiveElements._unmountAll(); // this unregisters the GlobalKeys
    });
    ///...
  } catch (e, stack) {
    ///...
  } finally {
    Timeline.finishSync();
  }
}
複製程式碼

​ 這個方法看起來很長,實際在非debug模式下只做了一件事。那就是釋放不活動的element。

4、總結

​ 不想再寫了,就把圖補全一下吧。這裡先把setState的流程圖再補完一下:

image-20210309203213963

再來看髒列表重新整理相關呼叫:

image-20210309211751151

相關文章