上一篇文章中,我們介紹 Flutter Widget 的設計思想、實現原理,并分析了 Widget、Element 和 RenderObject 的源碼,這篇文章繼續結合源碼分析 Flutter 的渲染過程。
實現原理
1,Flutter 渲染流程是怎樣的?
從這張圖上可知,界面顯示到屏幕上,Flutter 經過了 Vsync 信號、動畫、build、布局、繪制、合成等渲染過程。
顯示器垂直同步 Vsync 不斷的發出信號,驅動 Flutter Engine 刷新視圖。Flutter 提供 60 FPS,也就是一秒鐘發出60次信號,觸發60次重繪。
運行動畫,每個 Vsync 信號都會觸發動畫狀態改變。
Widget 狀態改變,觸發重建一棵新的 Widget 樹。比較新舊 Widget 樹的差異,找出有變動的 Widget 節點和對應的 RenderObject 節點。詳細過程請參考上一篇文章。
對需要更新 RenderObject 節點,更新界面布局、重新繪制。
根據新的 RenderObject 樹,更新合成圖層。
輸出新的圖層樹。
2,渲染過程中,Flutter 如何更新界面布局?
經過 build 環節后,找出需要更新的 RenderObject 樹,首先進入布局環節。上一篇文章中介紹到,element 被標記為 dirty 時便會重建,同樣的,RenderObject 被標記為 dirty 后,放入一個待處理的數組中。在布局環節中,遍歷該數組中的元素,按照節點在 RenderObject 樹上的深度重新布局。
每個節點 RenderObject 對象,按照部件的邏輯先計算自身所占空間的大小、位置,再計算 child 的,paren 將 size 約束限制傳遞給 child,child 根據這個約束計算自身的所占空間,然后再傳給 child 的 child,如此遞歸完成整個 RenderObject 樹的布局更新。大概過程如下
parent.performLayout() -> child.layout() -> child.performLayout()/child.performResize() -> child.child.layout() -> .....
Flutter 還有一個 RelayoutBoundary,用于確定重繪邊界,可以手動指定或自動設置。邊界內的對象重新布局,不會影響邊界外的對象。
3,渲染過程中,Flutter 如何繪制界面?
Paint 的過程有點類似于 Layout,同樣將待重新繪制的 RenderObject 標記為 dirty,放入一個數組中。這個數組也是深度優先的順序執行,先繪制自身,再繪制 child。
isRepaintBoundary 重繪邊界也類似上面的 RelayoutBoundary,重繪邊界內的元素及 child,會一起重新繪制,邊界外的元素不受影響。
源碼分析
我們按照 Flutter 的渲染依次分析 Vsync、build、layout、paint 四個過程 。
Vsync
垂直同步信號 Vsync 到來后,執行一系列動作開始界面的重新渲染,那么在哪里監聽 Vsync 信號,收到 Vsync 信號如何通知界面刷新?
上一篇文章介紹了 Widget build 實現過程,其中提到在 Flutter 應用啟動時,初始化了一個單例對象 WidgetsFlutterBinding,它是連接 Flutter engine sdk 和 Widget 框架的橋梁,它混合了 SchedulerBinding 和 RendererBinding。SchedulerBinding 提供了 window.onBeginFrame 和 window.onDrawFrame 回調,監聽刷新事件。
RendererBinding 在初始化方法 initInstances 中,addPersistentFrameCallback 向 persistentCallbacks 隊列添加了一個回調 _handlePersistentFrameCallback。
在收到從 Flutter engine 傳來的刷新事件時,調用 _handlePersistentFrameCallback 回調,也就是執行 drawFrame 方法。
// RendererBinding
void initInstances() {
...
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
// SchedulerBinding
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
那么 persistentCallbacks 隊列什么時候被執行?
這里先介紹一個類 Window,它是 Flutter engine 提供的一個圖形界面相關的接口,包括了屏幕尺寸、調度接口、輸入事件回調、圖形繪制接口和其他一些核心服務。Window 有一個繪制的回調方法 _onDrawFrame
當 Flutter engine 調用 _onDrawFrame 時,觸發 SchedulerBinding.handleDrawFrame 方法,這個方法里面遍歷執行已注冊的回調,即前面注冊的 drawFrame 方法。
// SchedulerBinding
void ensureFrameCallbacksRegistered() {
window.onBeginFrame ??= _handleBeginFrame;
window.onDrawFrame ??= _handleDrawFrame;
}
void handleDrawFrame() {
...
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
...
}
Vsync 信號到來,Flutter engine 調用 _onDrawFrame 方法啟動渲染流程,開啟一系列的動作。
Build
收到刷新事件后,先調用 WidgetsBinding.drawFame 方法。這個方法重建 Widget 樹,這一過程上篇文章有詳細介紹,這里不多做贅述。
//WidgetsBinding
void drawFrame() {
...
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
...
}
Layout
super.drawFrame()
會進入到 RenderBinding.drawFrame 方法,開始重新布局和繪制。
//RenderBinding
void drawFrame() {
pipelineOwner.flushLayout(); //布局
pipelineOwner.flushCompositingBits(); //重繪之前的預處理操作,檢查RenderObject是否需要重繪
pipelineOwner.flushPaint(); // 重繪
renderView.compositeFrame(); // 將需要繪制的比特數據發給GPU
pipelineOwner.flushSemantics();
}
flushLayout 方法內,遍歷 _nodesNeedingLayout 數組,_nodesNeedingLayout 內存放的是被標記為 dirty 的 RenderObject 元素。遍歷前先對 _nodesNeedingLayout 數組排序,按照深度優先的順序重新排列,即先處理上層節點再處理下層節點,然后遍歷每個元素重新布局。
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();
}
}
...
}
_layoutWithoutResize 調用 performLayout 方法,每個 RenderObject 子類都有不同的實現,以 RenderView 為例,它讀取配置中的 size,然后調用 child 的 layout 方法,并把 size 限制傳進去。同時將自身的布局標志 _needsLayout 設置為 false
void _layoutWithoutResize() {
...
performLayout();
markNeedsSemanticsUpdate();
...
_needsLayout = false;
markNeedsPaint();
}
void performLayout() {
_size = configuration.size;
if (child != null)
child.layout(new BoxConstraints.tight(_size));//調用child的layout
}
layout 方法中,傳入的兩個參數:constraints 表示 parent 對 child 的大小限制,parentUsesSize 表示 child 布局變化是否影響 parent,如果為 true,當 child 布局變化時,parent 會被標記為需要重新布局。
void layout(Constraints constraints, { bool parentUsesSize: false }) {
...
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
performResize();
}
performLayout();
...
}
sizedByParent 表示 child size 完成由 parent 決定,所以當 sizedByParent 為 true 時,child size 在 performResize 中確定。當 sizedByParent 為 false 時,執行 performLayout 計算自身 size,并調用自身的 child 布局,最終調用鏈就變成:
parent.performLayout() -> child.layout() -> child.performLayout()/child.performResize() -> child.child.layout() -> .....
RelayoutBoundary,用于確定重繪邊界。邊界內的對象重新布局,不會影響邊界外的對象。在 RenderObject 的 markNeedsLayout 方法中,從自身開始向 parent 查找到 relayoutBoundary,然后把它添加到待布局 _nodesNeedingLayout 數組中,等下次 Vsnc 信號到來時重新布局。
void markNeedsLayout() {
...
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
...
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
void markParentNeedsLayout() {
_needsLayout = true;
final RenderObject parent = this.parent;
if (!_doingThisLayoutWithCallback) {
parent.markNeedsLayout();
} else {
assert(parent._debugDoingThisLayout);
}
}
Paint
布局完成后開始繪制,繪制的入口是 flushPaint。類似于布局,將需要重新繪制的 RenderObject 標記為 dirty,同樣按照深度優先的順序遍歷 _nodesNeedingPaint 數組,每個元素都重新繪制。
void flushPaint() {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
...
}
paint 由具體的 RenderObject 類重寫,每個實現都不一樣。如果 RenderObject 有 child,執行自身的 paint 后,再執行 paintChild,調用鏈: paint() -> paintChild() -> paint() ...
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
...
OffsetLayer childLayer = child._layer;
if (childLayer == null) {
child._layer = childLayer = OffsetLayer();
} else {
childLayer.removeAllChildren();
}
final PaintingContext childContext = PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
childContext._stopRecordingIfNeeded();
}
void _paintWithContext(PaintingContext context, Offset offset) {
...
paint(context, offset);
...
}
void paintChild(RenderObject child, Offset offset) {
...
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
...
}
isRepaintBoundary 類似于上面布局中的 RepaintBoundary,它決定是否自身是否獨立繪制,如果為 true,則獨立繪制,否則隨 parent 一塊繪制。
最后將所有layer組合成Scene,然后通過 ui.window.render 方法,把 scene 提交給Flutter Engine
void compositeFrame() {
...
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
ui.window.render(scene);
scene.dispose();
} finally {
Timeline.finishSync();
}
}