Flutter框架分析分析系列文章:
《Flutter框架分析(三)-- Widget,Element和RenderObject》
《Flutter框架分析(四)-- Flutter框架的執行》
前言
前面幾篇文章介紹了Flutter框架的渲染流水線,window
,初始化以及Widget
,Element
和RenderObject
體系。其中對Widget
,Element
和RenderObject
的介紹主要是一些靜態的說明,瞭解了以上這些技術點之後,在這篇文章裡我們會通過動態執行的方式來介紹一下Flutter框架是如何執行的。
從之前介紹的渲染流水線可以知道,這個過程大致可以分為兩段操作。第一段是從State.setState()
到去engine那裡請求一幀,第二段就是Vsync訊號到來以後渲染流水線開始重建新的一幀最後送入engine去顯示。我們先來看第一段Flutter框架都做了什麼。
排程之前
先看一下State.setState()
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
複製程式碼
這裡會呼叫到Element
的markNeedsBuild()
函式。
void markNeedsBuild() {
if (!_active)
return;
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
複製程式碼
Element
首先看自己是不是active的狀態,不是的話就直接返回了,如果是“髒”(dirty)的狀態也直接返回,不是的話會置上這個狀態然後呼叫BuildOwner
的scheduleBuildFor()
函式,這個BuildOwner
我們之前介紹過,它的例項是在WidgetsBinding
初始化的時候構建的。每個Element
的都會持有BuildOwner
的引用。由其父Element
在mount
的時候設定。
void scheduleBuildFor(Element element) {
if (element._inDirtyList) {
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
複製程式碼
BuildOwner
會維護一個_dirtyElements
列表,所有被標記為“髒”(dirty)的element
都會被新增進去。在此之前會呼叫onBuildScheduled()
。這個函式是WidgetsBinding
初始化的時候設定給BuildOwner
的,對應的是WidgetsBinding._handleBuildScheduled()
。
void _handleBuildScheduled() {
ensureVisualUpdate();
}
複製程式碼
這裡會呼叫到ensureVisualUpdate()
。這個函式定義在SchedulerBinding
裡的
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
複製程式碼
函式ensureVisualUpdate()
會判斷當前排程所處的狀態,如果是在idle
(空閒)或者postFrameCallbacks
執行狀態則呼叫scheduleFrame()
。其他狀態則直接返回。下面這三個狀態正是渲染流水線執行的時候。
void scheduleFrame() {
if (_hasScheduledFrame || !_framesEnabled)
return;
window.scheduleFrame();
_hasScheduledFrame = true;
}
複製程式碼
在函式scheduleFrame()
裡我們看到了熟悉的window
。這裡就是通知engine去排程一幀的地方了。排程之後會置上_hasScheduledFrame
標誌位,避免重複請求。另外一個標誌位_framesEnabled
是代表當前app的狀態,或者說其所處的生命週期是否允許重新整理介面。這個狀態有四種:resumed
,inactive
,paused
和suspending
。
resumed
:app可見且可以響應使用者輸入。inactive
:app不能響應使用者輸入,例如在Android上彈出系統對話方塊。paused
:app對使用者不可見。suspending
:app掛起??這個狀態貌似Android和iOS都沒有上報。
_framesEnabled
只有在resumed
和inactive
狀態下才為true
。也就是說,只有在這兩個狀態下Flutter框架才會重新整理頁面。
至此第一階段,也就是排程之前的工作做完了。看起來比較簡單,主要就是把需要重建的Element
放入_dirtyElements
列表。接下來Flutter框架會等待Vsync訊號到來以後engine回撥框架,這就是第二段要做的事情了。
Vsync到來之後
我們之前說過Vsync訊號到來之後,engin會按順序回撥window
的兩個回撥函式:onBeginFrame()
和onDrawFrame()
。這兩個回撥是SchedulerBinding
初始化的時候設定給window
的。對應的是SchedulerBinding.handleBeginFrame()
和SchedulerBinding.handleDrawFrame()
。
onBeginFrame
這個回撥會直接走到SchedulerBinding.handleBeginFrame()
。
void handleBeginFrame(Duration rawTimeStamp) {
...
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
_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;
}
}
複製程式碼
這個函式主要是在依次回撥“Transient”回撥函式,這些回撥函式是在排程之前設定在SchedulerBinding
裡的,這裡的“Transient”意思是臨時的,或者說是一次性的。原因是這些回撥函式只會被呼叫一次。注意看程式碼裡_transientCallbacks
被置為空Map
了。如果想在下一幀再次呼叫的話需要提前重新設定回撥。這些回撥主要和動畫有關係。也就是渲染流水線裡的第一階段,動畫(Animate)階段。關於動畫後續我會再寫文章從框架角度分析一下動畫的機制。
在執行回撥之前_schedulerPhase
的狀態被設定為SchedulerPhase.transientCallbacks
。回撥處理完以後狀態更新至SchedulerPhase.midFrameMicrotasks
意思是接下來會處理微任務佇列。處理完微任務以後,engine會接著回撥onDrawFrame()
。
onDrawFrame
這個回撥會直接走到SchedulerBinding.handleDrawFrame()
。
void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null;
}
}
複製程式碼
在handleDrawFrame
裡按順序處理了兩類回撥,一類叫“Persistent”回撥,另一類叫“Post-Frame”回撥。
“Persistent”字面意思是永久的。這類回撥一旦註冊以後是不能取消的。主要用來驅動渲染流水線。渲染流水線的構建(build),佈局(layout)和繪製(paint)階段都是在其中一個回撥裡的。
“Post-Frame”回撥主要是在新幀渲染完成以後的一類呼叫,此類回撥只會被呼叫一次。
在執行“Persistent”回撥之前_schedulerPhase
狀態變為SchedulerPhase.persistentCallbacks
。在執行“Post-Frame”回撥之前_schedulerPhase
狀態變為SchedulerPhase.postFrameCallbacks
。最終狀態變為SchedulerPhase.idle
。
這裡我們主要關注一個“Persistent”回撥:WidgetsBinding.drawFrame()
。這個函式是在RendererBinding
初始化的時候加入到“Persistent”回撥的。
void drawFrame() {
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
buildOwner.finalizeTree();
} finally {
...
}
}
複製程式碼
這裡首先會呼叫buildOwner.buildScope(renderViewElement)
。其入參renderViewElement
是element tree的根節點。此時渲染流水線就進入了構建(build)階段。接下來呼叫了super.drawFrame()
。這個函式定義在RendererBinding
中。
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
複製程式碼
可以看出渲染流水線的接力棒傳到了pipelineOwner
的手裡,渲染流水線就進入了佈局(layout)階段和繪製(paint)階段。關於最後這兩個階段本篇不做詳細介紹。這裡大家只要知道繪製完成以後Flutter框架最終會呼叫window.render(scene)
將新幀的資料送入engine顯示到螢幕。
最後呼叫buildOwner.finalizeTree();
。這個函式的作用是清理不再需要的Element
節點。在element tree更新以後可能有些節點就不再需要掛載在樹上了,在finalizeTree()
的時候會將這些節點及其子節點unmount。
構建(build)階段
void buildScope(Element context, [VoidCallback callback]) {
try {
_scheduledFlushDirtyElements = true;
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild();
} catch (e, stack) {
...
}
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
}
}
複製程式碼
還記得在排程幀之前會把需要更新的Element
標記為“髒”(dirty)並放入BuildOwner
的_dirtyElements
列表。這裡Flutter會先按照深度給這個列表排個序。因為Element
在重建的時候其子節點也都會重建,這樣如果父節點和子節點都為“髒”的話,先重建父節點就避免了子節點的重複重建。
排完序就是遍歷_dirtyElements
列表。依次呼叫Element.rebuild()
。這個函式又會呼叫到Element.performRebuild()
。我們之前介紹Element
的時候說過performRebuild()
由其子類實現。
我們之前的出發點是State.setState()
。那就先看看StatefulElement
如何做的。它的performRebuild()
是在其父類ComponentElement
裡:
void performRebuild() {
Widget built;
built = build();
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
...
}
}
複製程式碼
回憶一下ComponentElement
。這個build()
函式最終會呼叫到State.build()
了。返回的就是我們自己例項化的Widget
。拿到這個新Widget
就去呼叫updateChild()
。之前在講Element
的時候我們介紹過updateChild()
這個函式。由增,刪,改這麼幾種情況,對於MyWidget
,從State.setState()
過來是屬於改的情況。此時會呼叫child.update(newWidget);
。這個update()
函式又是由各個Element
子類實現的。這裡我們只列舉幾個比較典型的。
StatefulElement
和StatelessElement
的update()
函式最終都會呼叫基類Element
的rebuild()
函式。好像在兜圈圈的感覺。。。
RenderObjectElement
的update()
函式就比較簡單了
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
複製程式碼
更新只是呼叫了一下RenderObjectWidget.updateRenderObject()
。這個函式我們之前介紹過,只是把新的配置設定到現有的RenderObject
上。
回到上面那個兜圈圈的問題。理清這裡的呼叫關係的關鍵就是要搞清楚是此時的Element
是在對自己進行操作還是對孩子進行操作。假設我們有這樣的一個三層element tree進行更新重建。
父(StatefulElement)
子(StatefulElement)
孫(LeafRenderObjectElement)
複製程式碼
那麼從父節點開始,呼叫順序如下:
父.rebuild()--->父.performRebuild()--->父.updateChild()--->子.update()--->子.rebuild()--->子.performRebuild()--->子.updateChild()--->孫.update()。
可見構建(build)過程是從需要重建的Element
節點開始一層層向下逐個更新子節點。直到遇到葉子節點為止。
至此渲染流水線的構建(build)階段就跑完了。接下來就由pipelineOwner
驅動開始佈局(layout)和繪製(paint)階段了。這兩個階段留待以後再給大家介紹一下。
總結
本篇文章從我們熟悉的State.setState()
函式出發,大致介紹了Flutter框架是如何執行渲染流水線的。總體來說其執行時分為兩個階段,向engine排程幀之前和Vsync訊號到來engine回撥Flutter框架之後。剩餘篇幅則是以更新Element
為例介紹了一下渲染流水線的構建(build)階段都做了一些什麼事情。限於篇幅,沒有更多涉及Element
的新增和刪除步驟。大家感興趣的話可以直接看原始碼來了解相關資訊。