Flutter 核心原理與混合開發模式

在 「Fan 直播」的 Flutter 混合開發實踐中,咱們總結了一些 Flutter 混合開發的經驗。本文第一篇章將從 Flutter 原理出發,詳細介紹 Flutter 的繪製原理,藉由此在第二章來對比三種跨端方案;以後再進入第三篇章 Flutter 混合開發模式的講解,主要是四種不一樣的 Flutter 混合模式的原理分析;最後的第四篇章,簡單分享一下混合工程的工程化探索。html

「惟有深刻,方能淺出」,對於一門技術,只有瞭解的深刻,才能用最淺顯、通俗的話語描述出。在此以前,我寫過一些 Flutter 的文章,但性質更偏向於學習筆記與源碼閱讀筆記,所以較爲晦澀,且零碎繁亂。本文做爲階段性的總結,我儘量以淺顯易懂的文字、按部就班地來分享 Flutter 混合開發的知識,對於關鍵內容會輔以源碼或源碼中的關鍵函數來解讀,但不會成段粘貼源碼。源碼學習的效果主要在於自身,因此若對源碼學習感興趣的,能夠自行閱讀 Framework 與 Engine 的源碼,也能夠閱讀我過往的幾篇文章。前端

好了,那廢話很少說,直接開始吧!android

1. Flutter 核心原理

1.1 Flutter 架構

flutter 架構

注:此圖引自 Flutter System Overviewios

傳統慣例,只要說到 Flutter 原理的文章,在開頭都會擺上這張圖。不論講的好很差,都是先擺出來,而後大部分仍是靠自行領悟。由於這張圖實在太好用了。c++

擺出這張圖,仍是簡單從總體上來先認識了一下什麼是 Flutter,不然容易陷入「盲人摸象」的境地。git

Flutter 架構採用分層設計,從下到上分爲三層,依次爲:Embedder、Engine、Framework。github

  1. Embedder:操做系統適配層,實現渲染 Surface 設置、線程設置等。
  2. Engine:實現 FLutter 渲染引擎、文字排版、事件處理、Dart 運行時等功能。包括了 Skia 圖形繪製庫、Dart VM、Text 等,其中 Skia 和 Text 爲上層接口提供了調用底層渲染和排版的能力。
  3. Framework:是一個用 Dart 實現的 UI SDK,從上之下包括了兩大風格組件庫、基礎組件庫、圖形繪製、手勢識別、動畫等功能。

至於更多詳情,這張圖配合源碼食用體驗會更好。但因爲本文不是源碼解析,因此這個工做本文就不展開了。接下來,我會以 Flutter 繪製流程爲例,來說解 Flutter 是如何工做的。這也能更好地幫助你理解源碼的思路。算法

1.2 Flutter 繪製原理

Flutter 繪製流程總結了一下大致上以下圖所示:編程

flutter-render.png

首先是用戶操做,觸發 Widget Tree 的更新,而後構建 Element Tree,計算重繪區後將信息同步給 RenderObject Tree,以後實現組件佈局、組件繪製、圖層合成、引擎渲染。canvas

做爲前置知識,咱們先來看看渲染過程當中涉及到的數據結構,再來具體剖析渲染的各個具體環節。

1.3 Flutter 渲染過程當中的數據結構

Data Model

渲染過程當中涉及到的關鍵的數據結構包括三棵樹和一個圖層,其中 RenderObject 持有了 Layer,咱們重點先看一下三棵樹之間的關係。

舉個栗子,好比有這麼一個簡單的佈局:

佈局

那麼對應的三棵樹之間的關係以下圖所示:

三棵樹

1.3.1 Widget Tree

Widget Tree

第一棵樹,是 Widget Tree。它是控件實現的基本邏輯單位,是用戶對界面 UI 的描述方式。

須要注意的是,Widget 是不可變的(immutable),當視圖配置信息發生變化時,Flutter 會重建 Widget 來進行更新,以數據驅動 UI 的方式構建簡單高效。

那爲何將 Widget Tree 設計爲 immutable?Flutter 界面開發是一種響應式編程,主張「simple is fast」,而由上到下從新建立 Widget Tree 來進行刷新,這種思路比較簡單,不用額外關係數據更變了會影響到哪些節點。另外,Widget 只是一個配置是數據結構,建立是輕量的,銷燬也是作過優化的,不用擔憂整棵樹從新構建帶來的性能問題。

1.3.2 Element Tree

Element Tree

第二棵樹,Element Tree。它是 Widget 的實例化對象(以下圖,Widget 提供了 createElement 工廠方法來建立 Element),持久存在於運行時的 Dart 上下文之中。它承載了構建的上下文數據,是鏈接結構化的配置信息到最終完成渲染的橋樑。

之因此讓它持久地存在於 Dart 上下文中而不是像 Widget 從新構建,**由於 Element Tree 的從新建立和從新渲染的開銷會很是大,**因此 Element Tree 到 RenderObject Tree 也有一個 Diff 環節,來計算最小重繪區域。

@immutable
abstract class Widget extends DiagnosticableTree {

  /// Initializes [key] for subclasses.
  const Widget({ this.key });
  final Key key;
  
  @protected
  @factory
  Element createElement();

  /// ... 省略其餘代碼
}
複製代碼

須要注意的是,Element 同時持有 Widget 和 RenderObject,但不管是 Widget 仍是 Element,其實都不負責最後的渲染,它們只是「發號施令」,真正對配置信息進行渲染的是 RenderObject。

1.3.3 RenderObject Tree

RenderObject Tree

第三棵樹,RenderObject Tree,即渲染對象樹。RenderObject 由 Element 建立並關聯到 Element.renderObject 上(以下圖),它接受 Element 的信息同步,一樣的,它也是持久地存在 Dart Runtime 的上下文中,是主要負責實現視圖渲染的對象。

RenderObject get renderObject {
  RenderObject result;
  void visit(Element element) {
    assert(result == null); // this verifies that there's only one child
    if (element is RenderObjectElement)
      result = element.renderObject;
    else
      element.visitChildren(visit);
  }
  visit(this);
  return result;
}
複製代碼

RenderObject Tree 在 Flutter 的展現過程分爲四個階段:

  1. 佈局
  2. 繪製
  3. 合成
  4. 渲染

其中,佈局和繪製在 RenderObject 中完成,Flutter 採用深度優先機制遍歷渲染對象樹,肯定樹中各個對象的位置和尺寸,並把它們繪製到不一樣的圖層上。繪製完畢後,合成和渲染的工做則交給 Skia 處理。

那麼問題來了,爲何是三棵樹而不是兩棵?爲何須要中間的 Element Tree,由 Widget Tree 直接構建 RenderObject Tree 不能夠嗎?

理論上能夠,但實際不可行。由於若是直接構建 RenderObject Tree 會極大地增長渲染帶來的性能損耗。由於 Widget Tree 是不可變的,但 Element 倒是可變的。**實際上,Element 這一層將 Widget 樹的變化作了抽象(相似 React / Vue 的 VDOM Diff),只將真正須要修改的部分同步到 RenderObject Tree 中,由此最大程度去下降重繪區域,提升渲染效率。**能夠發現,Flutter 的思想很大程度上是借鑑了前端響應式框架 React / Vue。

對比

此外,再擴展補充一下 VDOM。咱們知道,Virtual DOM 的幾個優點是:

  1. Diff 算法,保證操做盡量少的 DOM 節點。這裏在 Flutter 的 Element Tree 中體現了出來。
  2. UI 聲明式編程,代碼可維護性強。這一點在 Dart 聲明式編寫 UI 組件的時候能夠體現出來。
  3. 將真實的節點抽象出來,能夠方便實現跨平臺。這一點在 Flutter 側沒有體現,由於 Flutter 自己就是跨端的自繪引擎。但換個思路,咱們構建 Element 的 Widget Tree 可否不用 Dart 構建,專用其餘支持運行時編譯的語言構建(如 JavaScript),那這樣不就能夠實現動態化了嗎?是的,目前 MXFlutter 就是以這種思路來實現動態化的。

1.3.4 Layers

layers

最後,看看 Layer,它依附於 RenderObject(經過 RenderObject.layer 獲取),是繪圖操做的載體,也能夠緩存繪圖操做的結果。Flutter 分別在不用的圖層上繪圖,而後將這些緩存了繪圖結果的圖層按照規則進行疊加,獲得最終的渲染結果,也就是咱們所說的圖像。

/// src/rendering/layer.dart

abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
  /// ... 省略無關代碼
  
  bool get alwaysNeedsAddToScene => false;
  bool _needsAddToScene = true;
  void markNeedsAddToScene() {
    _needsAddToScene = true;
  }
  
  bool _subtreeNeedsAddToScene;
  void updateSubtreeNeedsAddToScene() {
    _subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
  }
}
複製代碼

如上圖代碼所示,Layer 的基類上有兩個屬性 _needsAddToScene 和 _subtreeNeedsAddToScene,前者表示須要加入場景,後者表示子樹須要加入場景。一般,只有狀態發生了更新,才須要加入到場景,因此這兩個屬性又能夠直觀理解爲「本身須要更新」和「子樹須要更新」。

Layer 提供了 markNeedsAddToScene() 來把本身標記爲「須要更新」。派生類在本身狀態發生變化時調用此方法把本身標記爲「須要更新」,好比 ContainerLayer 的子節點增刪、OpacityLayer 的透明度發生變化、PictureLayer 的 picture 發生變化等等。

1.4 Flutter 繪製流程拆解

繪製流程分爲如下六個階段:

  1. Build
  2. Diff
  3. Layout
  4. Paint
  5. Composite
  6. Render

拋開 Diff 和 Render 咱們本文不講解,由於這兩部分稍稍繁瑣一些,咱們來關注下剩下的四個環節。

繪製流程

注:此流程圖出自 複雜業務如何保證Flutter的高性能高流暢度?| 閒魚技術,能夠較爲清晰的表達 Flutter 核心的繪製流程了。

1.4.1 Build

執行 build 方法時,根據組件的類型,存在兩種不一樣的邏輯。

咱們知道,Flutter 內的 Widget 能夠分爲 StatelessWidget 與 StatefulWidget,即無狀態組件與有狀態組件。

所謂 StatelessWidget,就是它 build 的信息徹底由配置參數(入參)組成,換句話說,它們一旦建立成功就再也不關心、也不響應任何數據變化進行重繪。

StatelessWidget

所謂 StatefulWidget,除了父組件初始化時傳入的靜態配置以外,還要處理用戶的交互與內部數據變化(如網絡數據回包)並體如今 UI 上,這類組件就須要以 State 類打來 Widget 構建的設計方式來實現。它由 State 的 build 方法構建 UI, 最終調用 buildScope 方法。其會遍歷 _dirtyElements,對其調用 rebuild/build。

StatefulWidget

注:以上兩圖出自 《Flutter 核心技術與實戰 | 陳航》

1.4.2 Layout

只有佈局類 Widget 會觸發 layout(如 Container、Padding、Align 等)。

每一個 RenderObject 節點須要作兩件事:

  1. 調用本身的 performLayout 來計算 layout
  2. 調用 child 的 layout,把 parent 的限制傳入
/// 實際計算 layout 的實現
void performLayout() {
  _size = configuration.size;
  if (child != null) {
    child.layout(BoxConstraints.tight(_size));
  }
}

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  /// ...省略無關邏輯
  RenderObject relayoutBoundary;
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } else {
    relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
  }
  
  if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
    return;
  }

  _constraints = constraints;
  _relayoutBoundary = relayoutBoundary;

  if (sizedByParent) {
    performResize();
  }
  RenderObject debugPreviousActiveLayout;

  performLayout();
  markNeedsSemanticsUpdate();

  _needsLayout = false;
  markNeedsPaint();
}
複製代碼

如此遞歸一輪,每一個節點都受到父節點的約束並計算出本身的 size,而後父節點就能夠按照本身的邏輯決定各個子節點的位置,從而完成整個 Layout 環節。

layout

1.4.3 Paint

渲染管道中首先找出須要重繪的 RenderObject,若是有實現了 CustomPainter 則調用 CustomPainter paint 方法 再調用 child 的 paint 方法;若是未實現 CustomPainter,則直接調用 child 的 paint。

在調用 paint 的時候,通過一串的轉換後,layer->PaintingContext->Canvas,最終 paint 就是描繪在 Canvas 上。

void paint(PaintingContext context, Offset offset) {
  if (_painter != null) { 
    // 只有持有 CustomPainter 狀況下,才繼續往下調用自定義的 CustomPainter 的 paint 方法,把 canvas 傳過去
    _paintWithPainter(context.canvas, offset, _painter);
    _setRasterCacheHints(context);
  }
  super.paint(context, offset); //調用父類的paint的方法
  if (_foregroundPainter != null) {
    _paintWithPainter(context.canvas, offset, _foregroundPainter);
    _setRasterCacheHints(context);
  }
}

// 在父類的 paint 裏面繼續調用 child 的 paint,實現父子遍歷
void paint(PaintingContext context, Offset offset) {
  if (child != null){
    context.paintChild(child, offset); 
}

void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
  int debugPreviousCanvasSaveCount;
  canvas.save();
  if (offset != Offset.zero)
    canvas.translate(offset.dx, offset.dy);
  // 在調用 paint 的時候,通過一串的轉換後,layer->PaintingContext->Canvas,最終 paint 就是描繪在 Canvas 上
  painter.paint(canvas, size); 
  /// ...
  canvas.restore();
}
複製代碼

1.4.4 Composite

合成主要作三件事情:

  1. 把全部 Layer 組合成 Scene
  2. 經過 ui.window.render 方法,把 Scene 提交給 Engine。
  3. Engine 把計算全部的 Layer 最終的顯示效果,渲染到屏幕上。
final ui.Window _window;

void compositeFrame() {
  // 省略計時邏輯
  final ui.SceneBuilder builder = ui.SceneBuilder();
  final ui.Scene scene = layer.buildScene(builder);
  if (automaticSystemUiAdjustment)
    _updateSystemChrome();
  _window.render(scene);
  scene.dispose();
}

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

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;
  }
}

複製代碼

2. 跨端方案對比

跨端開發是必然趨勢,從本質上來講,它增長業務代碼的複用率,減小由於適配不一樣平臺帶來的工做量,從而下降開發成本。在各平臺差別抹平以前,要想「多快好省」地開發出各端體驗接近一致的程序,那即是跨端開發了。

總得來講,業內廣泛認同跨端方案存在如下三種:

  1. Web 容器方案
  2. 泛 Web 容器方案
  3. 自繪引擎方案

下面來一一講解。

2.1 Web 容器

跨端方案1

所謂 Web 容器,便是基於 Web 相關技術經過瀏覽器組件來實現界面和功能,包括咱們一般意義上說的基於 WebView 的 「H5」、Cordova、Ionic、微信小程序。

這類 Hybrid 開發模式,只須要將開發一次 Web,就能夠同時在多個系統的瀏覽器組件中運行,保持基本一致的體驗,是迄今爲止熱度很高的跨端開發模式。而 Web 與 原生系統之間的通訊,則經過 JSBridge 來完成,原生系統經過 JSBridge 接口暴露能力給 Web 調用。而頁面的呈現,則由瀏覽器組件按照標準的瀏覽器渲染流程自行將 Web 加載、解析、渲染。

這類方案的優勢:簡單、自然支持熱更新、生態繁榮、兼容性強、開發體驗友好。

固然,缺點也很明顯,不然就沒有後面兩個方案什麼事了,主要是體驗上的問題:

  1. 瀏覽器渲染流程複雜,頁面須要在線加載,體驗受限於網絡。因此 Web 存在白屏時間(PWA 例外)、且交互上體驗上與原生體驗有着很是很是明顯區別。
  2. 雙端須要分別實現 JSBridge 接口,且 JSBridge 的通訊效率通常。

2.2 泛 Web 容器

跨端方案2

因此輪到泛 Web 容器方案出場了,表明性框架是 React Native,Weex,Hippy。

  • 它放棄了瀏覽器渲染,而採用原生控件,從而保證交互體驗;
  • 它支持內置離線包,來規避加載耗時避免長時間白屏;
  • 它依然採用前端友好的 JavaScript 語言,來保證開發體驗。

在跨端通訊上,React Native 依然經過 Bridge 的方式來調用原生提供的方法。

這套方案理想是美好的,但現實確實骨感的,它在實踐下來以後也依然發現了問題:

  1. 直接調用原生控件雖然提高了體驗和性能,可是不一樣端相同的原生控件的渲染結果是存在差別的,跨端的差別須要巨大的工做量來抹平。
  2. Bridge 的通訊效率通常,在須要高頻通訊的場景下會形成丟幀。

2.3 自繪引擎

那咱們究竟能不能既簡單地抹平差別,又同時保證性能呢?

答案是能夠,那就是自繪引擎。不調用原生控件,咱們本身去畫。那就是 Flutter。比如警察問 React Native 嫌疑犯長什麼樣子,React Native 只能繪聲繪色地去描繪嫌疑犯的外觀,警察畫完以後再拿給 React Native 看,React Native 還要回答像不像;但 Flutter 本身就是一個素描大師,它能夠本身將嫌疑犯的畫像畫好而後交給警察看。這二者的效率和表現差別,不言而喻。

  1. 其經過 Skia 圖形庫直接調用 OpenGL 渲染,保證渲染的高性能,同時抹平差別性。
  2. 開發語言選擇同時支持 JIT 和 AOT 的 Dart,保證開發效率的同時,較 JavaScript 而言,更是提高了數十倍的執行效率。

跨端方案3

經過這樣的思路,Flutter 能夠儘量地減小不一樣平臺之間的差別, 同時保持和原生開發同樣的高性能。而且對於系統能力,能夠經過開發 Plugin 來支持 Flutter 項目間的複用。因此說,Flutter 成了三類跨端方案中最靈活的那個,也成了目前業內受到關注的框架。

至於通訊效率,Fluter 跨端的通訊效率也是高出 JSBridge 許許多多。Flutter 經過 Channel 進行通訊,其中:

  1. BasicMessageChannel,用於傳遞字符串和半結構化的信息,是全雙工的,能夠雙向請求數據。
  2. MethodChannel,用於傳遞方案調用,即 Dart 側能夠調用原生側的方法並經過 Result 接口回調結果數據。
  3. EventChannel:用戶數據流的通訊,即 Dart 側監聽原生側的實時消息,一旦原生側產生了數據,當即回調給 Dart 側。

其中,MethodChannel 在開發中用的比較多,下圖是一個標準的 MethodChannel 的調用原理圖:

MethodChannel

但爲何咱們說 Channel 的性能高呢?梳理一下 MethodChannel 調用時的調用棧,以下圖所示:

通訊效率

能夠發現,整個流程中都是機器碼的傳遞,而 JNI 的通訊又和 JavaVM 內部通訊效率同樣,整個流程通訊的流程至關於原生端的內部通訊。可是也存在瓶頸。咱們能夠發現,methodCall 須要編解碼,其實主要的消耗都在編解碼上了,所以,MethodChannel 並不適合傳遞大規模的數據。

好比咱們想調用攝像頭來拍照或錄視頻,但在拍照和錄視頻的過程當中咱們須要將預覽畫面顯示到咱們的 Flutter UI中,若是咱們要用 MethodChannel 來實現這個功能,就須要將攝像頭採集的每一幀圖片都要從原生傳遞到 Dart 側中,這樣作代價將會很是大,由於將圖像或視頻數據經過消息通道實時傳輸必然會引發內存和 CPU 的巨大消耗。爲此,Flutter 提供了一種基於 Texture 的圖片數據共享機制。

Texture 和 PlatformView 不在本文的探討範圍內,這裏就再也不深刻展開了,有興趣的讀者能夠自行查閱相關資料做爲擴展知識瞭解。

那接下來,咱們就進入本文的第三篇章吧,Flutter 混合開發模式的探索。

3. Flutter 混合開發模式

3.1 混合模式

Flutter 混合工程的結構,主要存在如下兩種模式:

  1. 統一管理模式
  2. 三端分離模式

兩種混合模式

所謂統一管理模式,就是一個標準的 Flutter Application 工程,而其中 Flutter 的產物工程目錄(ios/android/ )是能夠進行原生混編的工程,如 React Native 進行混合開發那般,在工程項目中進行混合開發就好。可是這樣的缺點是當原生項目業務龐大起來時,Flutter 工程對於原生工程的耦合就會很是嚴重,當工程進行升級時會比較麻煩。所以這種混合模式只適用於 Flutter 業務主導、原生功能爲輔的項目。但早期 Google 未支持 Flutter Module 時,進行混合開發也只存在這一種模式。

後來 Google 對混合開發有了更好的支持,除了 Flutter Application,還支持 Flutter Module。所謂 Flutter Module,恰如其名,就是支持以模塊化的方式將 Flutter 引入原生工程中,**它的產物就是 iOS 下的 Framework 或 Pods、Android 下的 AAR,原生工程就像引入其餘第三方 SDK 那樣,使用 Maven 和 Cocoapods 引入 Flutter Module 便可。**從而實現真正意義上的三端分離的開發模式。

3.2 混合棧原理

混合棧

爲了問題的簡潔性,咱們這裏暫時不考慮生命週期的統一性和通訊層的實現,而除此以外,混合導航棧主要須要解決如下四種場景下的問題:

  1. Native 跳轉 Flutter
  2. Flutter 跳轉 Flutter
  3. Flutter 跳轉 Native
  4. Native 跳轉 Native

3.2.1 Native 跳轉 Flutter

Native -> Flutter,這種狀況比較簡單,Flutter Engine 已經爲咱們提供了現成的 Plugin,即 iOS 下的 FlutterViewController 與 Android 下的 FlutterView(自行包裝一下能夠實現 FlutterActivity),因此這種場景咱們直接使用啓動了的 Flutter Engine 來初始化 Flutter 容器,爲其設置初始路由頁面以後,就能夠以原生的方式跳轉至 Flutter 頁面了。

// Existing code omitted.
// 省略已經存在的代碼
- (void)showFlutter {
  FlutterViewController *flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
  [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
複製代碼

3.2.2 Flutter 跳轉 Flutter

Flutter -> Flutter,業內存在兩種方案,後續咱們會詳細介紹到,分別是:

  1. 使用 Flutter 自己的 Navigator 導航棧
  2. 建立新的 Flutter 容器後,使用原生導航棧

3.2.3 Flutter 跳轉 Native

Flutter -> Native,須要注意的時,這裏的跳轉實際上是包含了兩種狀況:

  1. 打開原生頁面(open,包括但不限於 push)
  2. 回退到原生頁面(close,包括但不限於 pop)。

flutter->native

如上圖,這種狀況相對複雜,咱們須要使用 MethodChannel 讓 Dart 與 Platform 側進行通訊,Dart 發出 open 或 close 的指令後由原生側執行相應的邏輯。

3.2.4 Native 跳轉 Native

Native -> Native,這種狀況沒有什麼好說的,直接使用原生的導航棧便可。

3.3 混合模式

爲了解決混合棧問題,以及彌補 Flutter 自身對混合開發支持的不足,業內提出了一些混合棧框架,總得來講,離不開這四種混合模式:

  1. Flutter Boost 爲表明的類 WebView 導航棧
  2. Flutter Thrio 爲表明的 Navigator 導航棧
  3. 多 Engine 混合模式
  4. View 級別的混合模式

下面,一一來談談它們的原理與優缺點。

3.3.1 Flutter Boost

Flutter Boost 是閒魚團隊開源的 Flutter 混合框架,成熟穩定,業內影響力高,在導航棧的處理思路上沒有繞開咱們在 3.2 節中談及的混合棧原理,但須要注意的是,當 Flutter 跳轉 Flutter 時,它採用的是 new 一個新的 FlutterViewController 後使用原生導航棧跳轉的方式,以下圖所示:

flutter boost

這麼作的好處是使用者(業務開發者)操做 Flutter 容器就如同操做 WebView 同樣,而 Flutter 頁面就如同 Web 頁面,邏輯上簡單清晰,將全部的導航路由邏輯收歸到原生側處理。以下圖,是調用 open 方法時 Flutter Boost 的時序圖(關鍵函數路徑),這裏能夠看到兩點信息:

  1. 混合導航棧的邏輯主要包括原生層、通訊層、Dart 層。
  2. Flutter Boost 的 open 方法實現邏輯相對簡單。

flutter boost open 時序圖

可是它也有缺點,就是每次打開 Flutter 頁面都須要 new 一個 ViewController,在連續的 Flutter 跳轉 Flutter 的場景下有額外的內存開銷。針對這個問題,又有團隊開發了 Flutter Thrio。

3.3.2 Flutter Thrio

上面咱們說到,Flutter 跳轉 Flutter 這種場景 Flutter Boost 存在額外的內存開銷,故哈囉出行團隊今年4月開源了 Flutter Thrio 混合框架,其針對 Flutter Boost 作出的最重要的改變在於:Flutter 跳轉 Flutter 這種場景下,Thrio 使用了 Flutter Navigator 導航棧。以下圖所示:

flutter thrio

在連續的 Flutter 頁面跳轉場景下,內存測試圖表以下:

內存增量

從這張圖表中咱們能夠獲得如下幾點信息:

  1. 紅色區域是啓動 Flutter Engine 的內存增量,基本接近 30MB,Flutter Engine 是一個比較重的對象。
  2. FlutterViewController 帶來的內存增量廣泛在 12~15MB 左右。

可見,在這種場景下,Thrio 仍是作出了必定的優化的。但與之帶來的,就是實現的複雜性。咱們談到 Flutter Boost 的優勢是簡單,路由所有收歸原生導航棧。而 Flutter Thrio 混用了原生導航棧和 Flutter Navigator,所以實現會相對更復雜一下。這裏我梳理了一下 Flutter Thrio open 時關鍵函數路徑,能夠看到,Thrio 的導航管理確實是複雜了一些。

thrio 時序圖

3.3.3 多 Engine 模式

以上咱們談及的兩種混合框架都是單引擎的,對應的,也存在多引擎的框架。在談多引擎以前,仍是須要先介紹一下關於 Engine、Dart VM、isolate 幾個前置知識點。

在第一篇章中咱們沒有涉及到 Engine 層的源碼分析,而着重篇幅去講解 Framework 層的原理,一是爲了第一章的連貫性,二是此處也會單獨說到 Engine,仍是最好放在此時講解會更便於記憶與理解。

Dart VM、Engine 與 isolate

(a)Dart 虛擬機建立完成以後,須要建立 Engine 對象,而後會調用 DartIsolate::CreateRootIsolate() 來建立 isolate。 (b)每個 Engine 實例都爲 UI、GPU、IO、Platform Runner 建立各自新的 Thread。 (c)isolate,顧名思義,內存在邏輯上是隔離的。 (d)isolate 中的 code 是按順序執行的,任何 Dart 程序的併發都是運行多個 isolate 的結果。固然咱們能夠開啓多個 isolate 來處理 CPU 密集型任務。

根據(a)咱們能夠推出:(1) 每一個 Engine 對應一個 isolate 對象,即 Root Isolate。 根據(b)咱們能夠推出:(2) Engine 是一個比較重的對象(前文也有所說起)。 根據(c)和 (1) 咱們能夠推出:(3) Engine 與 Engine 之間相互隔離。 根據(d)和 (3) 咱們能夠推出:(4) Engine 沒有共享內存的併發,沒有競爭的可能性,不須要鎖,也就不存在死鎖問題。

好啦,記住這四個結論,咱們再來看看 window。

Window

window 是繪圖的窗口,也是鏈接 Flutter Framework(Dart)與 Flutter Engine(C++)的窗口 (5)。

從類的定義上來看,window 是鏈接 Framework 與 Engine 的窗口。在 Framework 層,window 指的是 ui.window 單例對象,源碼文件是 window.dart。而在 Engine 層,源碼文件是 window.cc,二者交互的 API 不多,可是一一對應:

/// window.dart
class Window {
  String/*!*/ _defaultRouteName() native 'Window_defaultRouteName';
  void scheduleFrame() native 'Window_scheduleFrame';
  String _sendPlatformMessage(String/*!*/ name, 
    PlatformMessageResponseCallback/*?*/ callback, 
    ByteData/*?*/ data) native 'Window_sendPlatformMessage';
  ByteData/*?*/ getPersistentIsolateData() native 'Window_getPersistentIsolateData';
  /// ...
}
複製代碼
// window.cc
void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
  natives->Register({
      {"Window_defaultRouteName", DefaultRouteName, 1, true},
      {"Window_scheduleFrame", ScheduleFrame, 1, true},
      {"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
      {"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
      {"Window_render", Render, 2, true},
      {"Window_updateSemantics", UpdateSemantics, 2, true},
      {"Window_setIsolateDebugName", SetIsolateDebugName, 2, true},
      {"Window_reportUnhandledException", ReportUnhandledException, 2, true},
      {"Window_setNeedsReportTimings", SetNeedsReportTimings, 2, true},
      {"Window_getPersistentIsolateData", GetPersistentIsolateData, 1, true},
  });
}
複製代碼

能夠發現,這些主要是 Framework 層調用 Engine 層中 Skia 庫封裝後的相關 API。那就不得不說說它的第二層含義——做爲繪圖的窗口。

從功能上來看,在界面繪製交互意義上,window 也是繪圖的窗口。在 Engine 中,繪圖操做輸出到了一個 PictureRecorder 的對象上;在此對象上調用 endRecording() 獲得一個 Picture 對象,而後須要在合適的時候把 Picture 對象添加(add)到 SceneBuilder 對象上;調用 SceneBuilder 對象的 build() 方法得到一個 Scene 對象;最後,在合適的時機把 Scene 對象傳遞給 window.render() 方法,最終把場景渲染出來。

window

該圖出自:Flutter Framework 源碼解析( 1 )—— 開篇和繪圖引擎的用法

實例代碼以下:

import 'dart:ui';

void main(){

  PictureRecorder recorder = PictureRecorder();
  Canvas canvas = Canvas(recorder);

  Paint p = Paint();
  p.strokeWidth = 30.0;
  p.color = Color(0xFFFF00FF);

  canvas.drawLine(Offset(300, 300), Offset(800, 800), p);

  Picture picture = recorder.endRecording();

  SceneBuilder sceneBuilder = SceneBuilder();
  sceneBuilder.pushOffset(0, 0);
  sceneBuilder.addPicture(new Offset(0, 0), picture);
  sceneBuilder.pop();
  Scene scene = sceneBuilder.build();

  window.onDrawFrame = (){
    window.render(scene);
  };
  window.scheduleFrame();
}
複製代碼
多 Engine 模式

綜上,根據(1)(3)(5)咱們能夠得出下圖的多引擎模式:

多 engine 模式

它有如下幾個特徵:

  1. App 內存在多個引擎
  2. 每一個引擎內有若干個 FlutterVC
  3. Engine 與 Engine 之間是隔離的

根據這三個特徵,咱們能夠設想一下其通訊層的實現,假設存在兩個引擎,每一個引擎內又存在兩個 FlutterVC,每一個 FlutterVC 內又存在兩個 Flutter 頁面,那這種場景下的跳轉就會變得很是複雜(下圖出自 Thrio 開源倉庫中的README):

多 engine 模式

因此顯而易見的,咱們不能否認 Engine 之間的邏輯隔離帶來了模塊間自然的隔離性,可是問題也有許多:

首先如上圖所示,通訊層設計會異常複雜,並且通訊層的核心邏輯依然是須要放在原生側來實現,如此便必定程度上失去了跨端開發的優點。

其次,咱們反覆提到 Engine 是一個比較重的對象,啓動多個 Flutter Engine 會致使資源消耗過多

最後,因爲 Engine 之間沒有共享內存,這種自然的隔離性其實弊大於利,在混合開發的視角下,一個 App 須要維護兩套緩存池——原生緩存池與 DartVM 所持有的緩存池,可是隨着開啓多 Engine 的介入,後者緩存池的資源又互不相通,致使資源開銷變得更加巨大

爲了解決傳統的多 Engine 模式所帶來的這些問題,又有團隊提出了基於 View 級別的混合模式。

3.3.4 View 級別的混合模式

基於 View 級別的混合模式,核心是爲每一個 window 加入 windowId 的概念,以便它們去共享同一份 Root Isolate。咱們剛纔說到,一個 isolate 具備一個 ui.window 單例對象,那麼只須要作一點修改,把 Flutter Engine 加入 ID 的概念傳給 Dart 層,讓 Dart 層存在多個 window,就能夠實現多個 Flutter Engine 共享一個 isolate 了。

以下圖所示:

View 級別的混合模式

這樣就能夠真正實現 View 級別的混合開發,能夠同時持有多份 FlutterViewController,且這些 FlutterVC 能夠內存共享

那缺點也比較明顯,咱們須要對 Engine 代碼作出修改,維護成本會很高。其次,多 Engine 的資源消耗問題在這種模式下也是須要經過對 Engine 不斷裁剪來解決的。

4. 工程化探索

4.1 編譯模式

Dart 自然支持兩種編譯模式,JIT 與 AOT。

4.1.1 JIT 與 AOT

所謂 JIT,Just In Time,即時編譯/運行時編譯,在 Debug 模式中使用,能夠動態下發和執行代碼,可是執行性能受運行時編譯影響。

JIT

所謂 AOT,Ahead Of Time,提早編譯/運行前編譯,在 Release 模式中使用,能夠爲特定平臺生成二進制代碼,執行性能好、運行速度快,但每次執行都須要提早編譯,開發調試效率低。

AOT

4.1.2 Debug、Release、Profile

對應的 Flutter App 存在三種運行模式:

  • Debug
  • Release
  • Profile

運行模式

所以,咱們能夠看出,在開發調試過程當中,咱們須要使用支持 JIT 的 Debug 模式,而在生產環境中,咱們須要構建包爲支持 AOT 的 Release 模式以保證性能。

那麼,這對咱們的集成與構建也提出了必定的要求。

4.2 集成與構建

所謂集成,指的是混合項目中,將 Flutter Module 的產物集成到原生項目中去,存在兩種集成方式,區別以下:

源碼集成 vs 產物集成

能夠發現源碼集成是 Flutter dev 分支須要的,可是產物集成是 Flutter dev 之外的分支須要的。在這裏,咱們的混合項目須要同時支持兩種不一樣的集成工程,在 Flutter dev 分支上進行源碼集成開發,而後依賴抽取構建產物發佈到遠程,如 iOS 構建成 pods 發佈到 Cocoapods 對應的倉庫,而 Android 構建成 AAR 發佈到 Maven 對應的雲端。因而,其餘分支的工程直接 gradle 或者 pod install 就能夠更新 Flutter 依賴模塊了。

構建

固然,咱們說到運行模式存在 Debug、Release、Profile 三種,其對應的集成產物也會區分這三種版本,但因爲產物集成沒法調試,集成 Debug 版本和 Profile 版本沒有意義,所以依賴抽取發佈時只須要發佈 Release 版本的產物就好。

4.3 工做流

在整套「Fan 直播」Flutter 混合項目搭建以後,咱們造成了一套初具雛形的 Flutter 工做流。在將來,咱們也會不斷完善 Flutter 混合開發模式,積極參與到 Flutter 的生態建設中去。

Flutter 工做流

擴展閱讀: Flutter 混合開發模式探索 Flutter Boost 混合開發實踐與源碼解析

相關文章
相關標籤/搜索