如何重新整理Flutter應用的一個介面?被問到這個問題,相信很多人第一個想到的都是setState
。沒錯,setState
方法確實可以讓State
的build
方法重走,從而達到重新整理介面的效果。但是你有沒有想過,為什麼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
下的onBeginFrame
和onDrawFrame
回撥。
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層了。通過註釋我們可以看到,onBeginFrame
和onDrawFrame
回撥將會被呼叫。
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
本質上是呼叫了element
的markNeedsBuild
方法。該方法會嘗試觸發幀的排程,然後把當前element
加入到BuildOwner
的髒列表中。在髒列表中的資料在一系列排程後,會被更新到螢幕上。
2、如何重新整理一個StatelessWidget
這個問題我之前在面試中被問過。當時我並沒有太深入閱讀Flutter的原始碼,因此當時我的回答是用外部重新整理。現在想想,回答的還是有點膚淺了。
雖然StatelessWidget
設計上是無狀態的,也沒有暴露任何重新整理的方法,但是我們想要去重新整理它也不是不可以的。上面分析了那麼多我們已經知道了,setState
重新整理一個StatefulWidget
的本質是呼叫element
的markNeedsBuild
方法來觸發更新,而StatelessWidget
自身也是有Element
的。因此我們只要呼叫了StatelessWidget
的Element
的markNeedsBuild
方法,就可以重新整理一個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
方法中我們儲存了StatelessWidget
的BuildContext
也就是StatelessElement。最開始我們展示的是測試字樣。點選後我們把文字更新為我被重新整理啦,然後手動呼叫markNeedsBuild
方法。
一開始執行後顯示的使我們設定的初始值:
點選後會重新整理成如下:
可以看到重新整理成功了。只要我們改變了build
方法下的描述內容,StatelessWidget
也是可以被重新整理的,當然並不推薦這麼做。
3、髒列表是如何被更新的
上文我們已經知道了setState
的本質是將當前的element
加入髒列表,髒列表中的資料在後續會被排程處理。現在我們來追蹤一下髒列表,看看它是如何被處理的。
BuildOwner -> buildScope
首先在BuildOwner
下我們發現髒列表會在buildScope
方法下被處理,接著追蹤該方法的呼叫:
WidgetsBinding -> drawFrame
void drawFrame() {
///...
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();//call super
buildOwner.finalizeTree();
}
///...
}
複製程式碼
由呼叫鏈我們追蹤到WidgetsBinding
的drawFrame
方法下呼叫了此方法,呼叫完成後會再呼叫父類的drawFrame
方法。現在需要再看看WidgetsBinding
的drawFrame
方法是如何被呼叫的:
可以看到只在一處被呼叫。
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();
}
複製程式碼
我們可以看到該方法被註冊為一個持久的監聽,每當有幀更新的時候都會被呼叫。該回撥具體的回撥時機我們已經分析過了,在觸發了window
的shceduleFrame
回撥後,會被呼叫,具體回撥的實現SchedulerBInding 的 handleDrawFrame方法中。
現在我們可以知道了髒列表更新的方法呼叫順序了:
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
的實現。
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
會進行佈局、混合、繪製等一系列的操作。最後通過RenderView
的compositeFrame
實際的去進行渲染一幀。
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
的流程圖再補完一下:
再來看髒列表重新整理相關呼叫: