Flutter 探索系列:布局和渲染(二)

上一篇文章中,我們介紹 Flutter Widget 的設計思想、實現原理,并分析了 Widget、Element 和 RenderObject 的源碼,這篇文章繼續結合源碼分析 Flutter 的渲染過程。

實現原理

1,Flutter 渲染流程是怎樣的?

image

從這張圖上可知,界面顯示到屏幕上,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();
  }
}

參考資料

Flutter
Flutter框架分析(四)-- Flutter框架的運行
Flutter運行機制-從啟動到顯示

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372