背靠大樹好乘涼——從代碼到圖像,Flutter 的繪製原理

上篇文章《背靠大樹好乘涼——Flutter 視圖渲染機制探究之三棵樹》探究了 Flutter 在渲染過程當中建立的三棵樹,它們分別是 Widget 樹、Element 樹和 RenderObject 樹。可是渲染過程可不僅是這三棵樹在發揮着做用,還有不少其餘的知識點咱們沒有涉及到,這篇文章咱們繼續討論。node

提出問題

咱們首先羅列出上篇文章中咱們沒有解釋的以及延伸上篇文章後的新產生一些問題:canvas

  • 既然 RenderObject 是真正的渲染對象,那麼爲何還須要 Element 呢?那爲何更多弄了個什麼 Widget 呢?一個 RenderObject 直接建立渲染不是很好嗎?
  • RenderObject 樹纔是 Flutter 的渲染樹,那在 RenderObject 樹建立完成後如何對樹進行渲染呢?
  • 上篇文章中又提到只有繼承自 RenderObjectWidget 的類纔是須要渲染的類,那相似於 Container 等一些類,它們繼承自 StatelessWidget 或 StatefulWidget,它們的諸如背景顏色等的屬性又是如何渲染到屏幕上的?

帶着這些問題,開始咱們今天的探索之旅。首先從 RenderObject 樹的渲染開始。性能優化

視圖渲染

幀回調

在討論渲染機制以前,咱們先回想咱們的 Flutter 工程,第一行代碼通常是這樣的:markdown

void main() {
  runApp(MyApp());
}
複製代碼

這行代碼雖然是模板生成的,可是它是咱們應用的開始,咱們思考一個問題,這個方法到底作了什麼?app

MyApp() 是咱們自定義 Widget 的根 Widget,那咱們就從這個方法出發,看一看 Flutter 到底是如何渲染和繪製的。框架

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}
複製代碼

首先經過 ensureInitialized() 獲取 WidgetsFlutterBinding 的單例,而後經過以下調用鏈將 Element 和 Widget 綁定,這部份內容咱們上篇文章已經討論過,不是咱們今天這篇文章須要討論的內容。less

咱們接着看下面的 scheduleWarmUpFrame() 方法。ide

void scheduleWarmUpFrame() {
    // ...
    
    // We use timers here to ensure that microtasks flush in between.
    Timer.run(() {
        assert(_warmUpFrame);
        handleBeginFrame(null);
    });
    Timer.run(() {
        assert(_warmUpFrame);
        handleDrawFrame();
        // ...
    });

    // ...
}
複製代碼

handleBeginFrame()handleDrawFrame() 方法分別被 Flutter engine 調用,分別在新的幀產生前的準備階段以及新的幀的產生階段,是一個時序上的差別。在這以前,咱們先了解一下 FrameCallback,在 Flutter 中(具體在 SchedulerBinding 類中)有三個 FrameCallback 的隊列——transientCallbackspersistentCallbacks 以及 postFrameCallbacks,它們分別在不一樣的時機被執行。transientCallbacks 正是在 handleBeginFrame() 方法中被執行,而 persistentCallbackspostFrameCallbacks 則是在 handleDrawFrame() 方法中被執行。它們的做用分別以下:佈局

  1. transientCallbacks:用於存放一些臨時回調,通常存放動畫回調。能夠經過SchedulerBinding.instance.scheduleFrameCallback 添加回調。
  2. persistentCallbacks:用於存放一些持久的回調,不能在此類回調中再請求新的繪製幀,持久回調一經註冊則不能移除。SchedulerBinding.instance.addPersitentFrameCallback(),這個回調中處理了佈局與繪製工做。
  3. postFrameCallbacks:在Frame結束時只會被調用一次,調用後會被系統移除,可由 SchedulerBinding.instance.addPostFrameCallback() 註冊,注意,不要在此類回調中再觸發新的Frame,這能夠會致使循環刷新。

說到這,咱們不妨來回憶一下,假如咱們有在視圖渲染完成後執行某些命令(如刷新視圖操做)的需求,咱們的作法是什麼。是否是一般會使用 WidgetsBinding.instance.addPostFrameCallback() 這個方法來等效實現,WidgetsBinding 正是 SchedulerBinding 的實現,因此這個方法實際上是藉助幀的完成來實現需求的。由於該回調在調用一次後會被移除,因此也免了咱們的後顧之憂。post

繪製

在成爲敲 Flutter 的小學生以前,我是一個敲 Android 的小學生,因此說到界面繪製,首先想到的就是那膾炙人口的三個流程——measure、layout 和 draw,那在 Flutter 中,繪製究竟是怎麼完成的呢?當咱們討論 Flutter 視圖渲染的時候,真正討論的實際上是 RenderObject 的渲染,當我在 Android studio 中查看 RenderObject 的 structure 時(通常默認快捷鍵是alt + 7),看到了 RenderObject 也有一個 layout 方法,而後經過以下的調用鏈定位到了 PipelineOwner 這個類。

PipelineOwner 管理着渲染的通道,它提供了驅動繪製通道的接口,並存儲了 RenderObject 在其各個時期都須要訪問的狀態。若是須要刷新繪製通道,需依次執行下面幾個方法:

  • flushLayout() 更新任何須要更新其佈局的 RenderObject,在這個階段,全部 RenderObject 的尺寸和佈局都已被計算過,但它們的狀態可能已被標記爲「dirty」,須要從新繪製。
  • flushCompositingBits() 更新任何 needsCompositing 變量被標記爲 true 的 RenderObject。在此階段,每一個 RenderObject 都獲悉它的孩子們是否須要從新組合,此信息被用於在繪製時期選擇如何實現視覺效果(如裁剪)。若是某個 RenderObject 有組合的孩子,則須要使用「Layer」來建立裁剪以使其裁剪能夠用於其組合的孩子(裁剪將被繪製進孩子本身的 Layer)。
  • flushPaint() 訪問任何須要被更新的 RenderObject。在這個階段,RenderObject 有機會將繪製命令記錄進 PictureLayer 中而且構造其餘的 Layer。
  • flushSemantics() 若是 semantic 被啓用,這個方法將爲 RenderObject 整合 semantic,semantic 通常用於語義反饋(如幫助視障人士的屏幕閱讀功能,更多相關信息還請自行搜索相關資料)。

那咱們就首先從 flushLayout() 方法看起:

/// PipelineOwner
void flushLayout() {
    // ...
    try {
        // TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves
        while (_nodesNeedingLayout.isNotEmpty) {
            final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
            _nodesNeedingLayout = <RenderObject>[];
            for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
                if (node._needsLayout && node.owner == this)
                    node._layoutWithoutResize();
            }
        }
    } finally {
        // ...
    }
}
複製代碼

在這個方法中,經過對 _nodesNeedingLayout 這個列表進行遍歷,而後調用其 _layoutWithoutResize() 方法。咱們在這裏稍做停留,暫且無論這個方法,_nodesNeedingLayout 這個隊列經過調用 RenderObject 的 markNeedsLayout() 方法,將 RenderObject 對象加入隊列,以待從新 layout。

在上篇中咱們知道,RenderObject 會經過調用 insert() 方法來插入對象成樹的,而在 insert() 方法中會執行 adoptChild() 方法,咱們看看代碼:

/// RenderObject
@override
void adoptChild(RenderObject child) {
    assert(_debugCanPerformMutations);
    assert(child != null);
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
}
複製代碼

此方法中則執行了 markNeedsLayout() 方法,即在 RenderObject 剛剛插入樹的時候即被標記爲須要從新佈局。而 markNeedLayout() 方法一樣會在 RenderObject 的各個實現中在適當的時機被調用(如尺寸位置等發生變化等), markNeedsCompositingBitsUpdate()markNeesPaint() 亦是同理。因此**當這些「髒節點」被標記完成以後,在下一個幀同步的信號到來時,會調用 RendererBinding 的 drawFrame() 方法從而使 flushLayout()flushCompositingBits() 和、flushPaint()flushSemantics() 依次得以執行。而這些方法的執行又會最終使得 RenderObject 的 layout 和 paint 等機制得以實現。**這就是 RenderObject 的渲染機制的主要流程了。下面咱們繼續向下看看它的實現細節。

layout

flushLayout() 中,會遍歷髒節點,而後依次執行其 _layoutWithoutResize() 方法,而後該方法會執行 performLayout() 方法,該方法在 RenderObject 類中未實現,被 RenderObject 的子類實現,而其子類實現中基本會調用其孩子的 layout() 方法,layout() 方法經過向下傳遞 Constraints 對象參數,對孩子節點進行尺寸約束(layout() 方法會調用 performResize() 方法進行尺寸設置 )。layout() 方法再調用同對象的 performLayout() 方法……依次造成以下的一個調用邏輯(RenderView 是 RenderObject 樹的根節點,該類沒有 layout() 方法)。

composite

檢查 RenderObject 是否須要重繪,而後更新 RenderObject.needsCompositing 屬性,若是該屬性值被標記爲true則須要重繪。

paint

flushPaint() 方法和 flushLayout() 方法相似,也是會遍歷以前被 markNeedsPaint() 方法標記爲髒的節點,依次將節點 RenderObject 傳入PaintingContext 的 repaintCompositedChild() 方法中做爲參數。

/// PipelineOwner
void flushPaint() {
    // ...
    try {
        final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
        _nodesNeedingPaint = <RenderObject>[];
        // Sort the dirty nodes in reverse order (deepest first).
        for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
            assert(node._layer != null);
            if (node._needsPaint && node.owner == this) {
                if (node._layer!.attached) {
                    PaintingContext.repaintCompositedChild(node);
                } else {
                    node._skippedPaintingOnLayer();
                }
            }
        }
        assert(_nodesNeedingPaint.isEmpty);
    } finally {
        // ...
    }
}
複製代碼

PaintingContext 類持有 Canvas 實例,repaintCompositedChild() 方法在調用 RenderObject 類的 _paintWithContext() 方法時,會將本身的實例做爲參數傳入,這樣,在接下來執行 paint() 方法時,其實例可以被 RenderObject 的子類在 paint() 方法的實現中獲取,從而進行繪製。咱們看一個 paint() 方法實現的例子,實現於類 _RenderRadio:

/// _RenderRadio
@override
void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

    paintRadialReaction(canvas, offset, size.center(Offset.zero));

    final Offset center = (offset & size).center;
    final Color radioColor = onChanged != null ? activeColor : inactiveColor;

    // Outer circle
    final Paint paint = Paint()
        ..color = Color.lerp(inactiveColor, radioColor, position.value)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2.0;
    canvas.drawCircle(center, _kOuterRadius, paint);

    // Inner circle
    if (!position.isDismissed) {
        paint.style = PaintingStyle.fill;
        canvas.drawCircle(center, _kInnerRadius * position.value, paint);
    }
}
複製代碼

什麼?還有第四棵樹?!

RepaintBoundary

上面咱們說過,markNeedsPaint() 方法用來標記是否須要重繪,但並無查看此方法的實現,如今咱們來看看它的源碼:

/// RenderObject
void markNeedsPaint() {
    assert(owner == null || !owner!.debugDoingPaint);
    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 as RenderObject;
        parent.markNeedsPaint();
        assert(parent == this.parent);
    } else {
        // ...
        // If we're the root of the render tree (probably a RenderView),
        // then we have to paint ourselves, since nobody else can paint
        // us. We don't add ourselves to _nodesNeedingPaint in this
        // case, because the root is always told to paint regardless.
        if (owner != null)
            owner!.requestVisualUpdate();
    }
}
複製代碼

這裏有一個 isRepaintBoundary() 變量,代碼中顯示,當此變量爲 false 且父節點是 RenderObject 類型時,父節點也會被標記爲須要重繪,那若是咱們的視圖樹很龐大,咱們但願每次重繪只重繪其中的某一小部分,那麼該如何處理?

官方對這個變量的解釋是「此 RenderObject 是否須要和它的 parent 分開重繪」。假如此變量爲 true,那麼 markNeedsPaint() 方法中就不會執行父節點(RenderObject)的標記重繪方法,那麼在重繪時就能將進度阻截在某一個小範圍內。

那這個變量如何設置其值爲 true 呢?在使用 CustomPaint 這個控件的時候,咱們可能在某些博客或者文章中看到推薦在其外面再包裹一個 RepaintBoundary 控件用於避免一些不須要的重繪操做。RepaintBoundary 繼承自 SingleChildRenderObjectWidget 類,所以它必實現了 createRenderObject() 方法,下面是其實現:

@override
RenderRepaintBoundary createRenderObject(BuildContext context) => RenderRepaintBoundary();
複製代碼

RenderRepaintBoundary 是 RenderObject 的一個子類,而它的 isRepaintBoundary 變量的值正是爲 true,因此若是須要達到上面部分重繪的目的,那麼咱們就可使用這個控件來幫助咱們實現。

Layer

若是你之前接觸過某些遊戲框架的話,那麼應該對 Layer 不陌生,能夠把它理解成一張畫紙,全部的控件都是畫在這張「畫紙」上的。那爲了對全部的控件有一個整理,應該將它們畫在不一樣的「畫紙」上,便於作一些統一的操做,好比一次性更新一張「畫紙」上的全部控件。沒錯,這個 Layer 也有一棵樹,它就是所謂的「第四棵樹」。

Layer 通常可分爲 ContainerLayer 和其餘 Layer,ContainerLayer 具有子節點,它又有以下子類:

  • 位移類(OffsetLayer/TransformLayer);
  • 透明類(OpacityLayer)
  • 裁剪類(ClipRectLayer/ClipRRectLayer/ClipPathLayer);
  • 陰影類 (PhysicalModelLayer)

PaintingContext 的 paintChild() 方法被 RenderObject 的子類調用以繪製孩子節點:

/// PaintingContext
void paintChild(RenderObject child, Offset offset) {
    // ...

    if (child.isRepaintBoundary) {
        stopRecordingIfNeeded();
        _compositeChild(child, offset);
    } else {
        child._paintWithContext(this, offset);
    }

    // ...
}
複製代碼

當孩子節點的 isRepaintBoundary 屬性值爲 true 時,不會再執行孩子節點的 _paintWithContext() 方法進行繪製,而是會調用 _compositeChild() 方法建立新的 Layer 並追加到原來的 Layer 樹中。

/// PaintingContext
void _compositeChild(RenderObject child, Offset offset) {
    // ...
    final OffsetLayer childOffsetLayer = child._layer as OffsetLayer;
    childOffsetLayer.offset = offset;
    appendLayer(child._layer!);
}

/// PaintingContext
@protected
void appendLayer(Layer layer) {
    assert(!_isRecording);
    layer.remove();
    _containerLayer.append(layer);
}

/// ContainerLayer
void append(Layer child) {
    // ...
    adoptChild(child);
    child._previousSibling = lastChild;
    if (lastChild != null)
        lastChild!._nextSibling = child;
    _lastChild = child;
    _firstChild ??= child;
    assert(child.attached == attached);
}
複製代碼

Container 的 append() 方法中的代碼是否是有點眼熟?對,在上篇文章介紹 MultiChildRenderObjectElement 的孩子節點插入操做時,其 _insertIntoChildList() 方法也是相似的雙鏈綁定操做。因此,與其說 Layer 樹,倒不如說 Layer 鏈。

Layer 成鏈後,若是須要物盡其用,那麼應該找到可以處理它的下家。

每一個 RenderObject 對象都有一個綁定的 layer 對象,在 RendererBinding 的 drawFrame() 方法中,會執行 RenderView 對象的 compositeFrame() 方法:

/// RenderView
void compositeFrame() {
    Timeline.startSync('Compositing', arguments: timelineArgumentsIndicatingLandmarkEvent);
    try {
        final ui.SceneBuilder builder = ui.SceneBuilder();
        final ui.Scene scene = layer!.buildScene(builder);
        if (automaticSystemUiAdjustment)
            _updateSystemChrome();
        _window.render(scene);
        scene.dispose();
        // ...
    } finally {
        Timeline.finishSync();
    }
}
複製代碼

這裏又接觸到了一個新的類 Scene(是否是愈來愈以爲像某些遊戲框架的設定),它是一個 native 類,對應 Flutter engine 中的 scene.cc,在 Flutter framework 中,Scene 只能經過 SceneBuilder 類建立。

咱們知道 RenderView 是 RenderObject 樹的根節點,那麼上面方法中的 layer 也是 layer 鏈的第一個節點,帶着這個概念,咱們進入 buildScene() 方法中看看:

/// ContainerLayer
ui.Scene buildScene(ui.SceneBuilder builder) {
    // ...
    updateSubtreeNeedsAddToScene();
    addToScene(builder);
    // Clearing the flag _after_ calling `addToScene`, not _before_. This is
    // because `addToScene` calls children's `addToScene` methods, which may
    // mark this layer as dirty.
    _needsAddToScene = false;
    final ui.Scene scene = builder.build();
    // ...
    return scene;
}

/// ContainerLayer
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
    addChildrenToScene(builder, layerOffset);
}

/// ContainerLayer
void addChildrenToScene(ui.SceneBuilder builder, [ Offset childOffset = Offset.zero ]) {
    Layer? child = firstChild;
    while (child != null) {
        if (childOffset == Offset.zero) {
            child._addToSceneWithRetainedRendering(builder);
        } else {
            child.addToScene(builder, childOffset);
        }
        child = child.nextSibling;
    }
}
複製代碼

上面的調用鏈的做用就是遍歷 Layer 鏈,而後執行它們的 addToScene() 方法,將 Layer 添加到 Scene,該方法聲明在 Layer 基類中,被其子類實現。

當全部的 Layer 都執行了 addToScene() 的操做後,RenderView 的 compositeFrame() 方法中執行 _window.render(scene) 方法就會將包括了全部 Layer 的 Scene 推給 GPU 執行渲染工做。

三棵樹都是須要的嗎

Flutter 渲染的過程上面已經介紹完了,咱們思考本文開頭提出的三個問題中的另外一個——既然 RenderObject 樹纔是負責渲染的樹,那麼 Flutter 爲何還會構建出另外兩棵樹?只維護 RenderObject 不是更省事嗎?Widget 樹既然和 Element 樹是一一對應的,那它們是否是多餘的呢?

對於這些問題,我也上網搜索了一些文章,並無找到很詳細說明這塊問題的。大概的說法都是出於性能上的考慮,由於渲染的代價很高,每次改動都全局渲染會拖垮系統。我在寫這篇文章的幾天裏,常常會想到這個問題,可是一直都沒有想到一棵樹沒法完成的性能優化的場景,由於一棵樹的狀況下也是徹底能夠作到局部渲染和有必要才渲染的。想到我之前的幾篇文章中提到的 Flutter 設計中常常採用的封裝思想,即把系統對用戶的暴露控制到最小,只暴露給用戶須要的接口,實現部分徹底由系統本身完成,我就開始懷疑這是否是也是徹底由於處於這樣的設計考量。因此做爲一個敲代碼的小學生,這個問題我暫時沒有辦法給出解答,也還在疑惑中,若是我後面有機會窺得其中奧妙,會再作修改和補充。能力不足,請多擔待。

其餘問題

上篇文章中說到,只有繼承自 RenderObjectWidget 的 widget 纔會有對應的 RenderObject 節點,纔是真正須要渲染的節點,但有小夥伴可能會有疑惑:「咱們最經常使用的 Text 控件,它是繼承自 StatelessWidget 的,難道它不會被渲染嗎?那屏幕上顯示的那些文字又是如何來的呢?」

針對這個問題,咱們看一下 Text 的相關源碼。

/// Text
const Text(
    this.data,
    // ..
)

/// Text
@override
Widget build(BuildContext context) {
    // ...
    Widget result = RichText(
        // ...
        text: TextSpan(
            style: effectiveTextStyle,
            text: data,
            children: textSpan != null ? <InlineSpan>[textSpan] : null,
        ),
    );
    // ...
    return result;
}
複製代碼

可見,咱們平時的代碼譬如 Text("Hello world") 中的文字之因此可以顯示是由於它是被包裹在 RichText 控件中的,而 RichText 類是繼承自 RenderObjectWidget(MultiChildRenderObjectWidget)的。

再好比 Container 類繼承自 StatelessWidget,那在給 Container 控件設置背景顏色時,它的顏色是如何渲染的呢?一樣在其 build() 方法中能夠看到顏色屬性被傳遞給了 ColoredBox 這個類,而這個類一樣繼承自 RenderObjectWidget(SingleChildRenderObjectWidget)。諸如此類,說明咱們在上一篇獲得的結論是站得住腳的。

最後

關於 Flutter 渲染機制的問題,就先討論這麼多,其實還有不少的問題咱們沒有涉及到,好比 BuildContext,好比 Element 和 State 的一些狀態和生命週期等,首先是這些都比較簡單,相信憑你們的智慧必定不是問題,其次是由於我以爲相比於渲染和繪製的過程來講,它們好像又沒有顯得那麼重要。這兩篇文章前先後後着實花了很多時間,可是完成了仍是很開心的,若有紕漏請指教。

今天(2020/12/28)是聖誕後的第三天,考研後的第一天,距離元旦還有4天。首先恭祝各位「雙旦」快樂,而後祝福各位考研的小夥伴一戰功成!

我是敲代碼的小學生,下篇見!

相關文章
相關標籤/搜索