激情六月丁香婷婷|亚洲色图AV二区|丝袜AV日韩AV|久草视频在线分类|伊人九九精品视频|国产精品一级电影|久草视频在线99|在线看的av网址|伊人99精品无码|午夜无码视频在线

高校合作1:010-59833514 ?咨詢電話:400-810-1418 服務(wù)與監(jiān)督電話:400-810-1418轉(zhuǎn)接2

flutter 游戲開發(fā)(Flutter 核心原理與混合開發(fā)模式)

發(fā)布時(shí)間:2023-11-27 21:31:22 瀏覽量:111次

?Flutter 核心原理與混合開發(fā)模式

flutter 游戲開發(fā)(Flutter 核心原理與混合開發(fā)模式)

作者:airingdeng,騰訊QQ前端開發(fā)工程師

本文將從 Flutter 原理出發(fā),詳細(xì)介紹 Flutter 的繪制原理,借由此來對(duì)比三種跨端方案;之后再進(jìn)入第三篇章 Flutter 混合開發(fā)模式的講解,主要是四種不同的 Flutter 混合模式的原理分析;最后簡單分享一下混合工程的工程化探索。

目錄:

  • Flutter 核心原理與混合開發(fā)模式
    • 1. Flutter 核心原理
    • 2. 跨端方案對(duì)比
    • 3. Flutter 混合開發(fā)模式
    • 4. 工程化探索


“唯有深入,方能淺出”,對(duì)于一門技術(shù),只有了解的深入,才能用最淺顯、通俗的話語描述出。在此之前,我寫過一些 Flutter 的文章,但性質(zhì)更偏向于學(xué)習(xí)筆記與源碼閱讀筆記,因此較為晦澀,且零碎繁亂。本文作為階段性的總結(jié),我盡可能以淺顯易懂的文字、循序漸進(jìn)地來分享 Flutter 混合開發(fā)的知識(shí),對(duì)于關(guān)鍵內(nèi)容會(huì)輔以源碼或源碼中的關(guān)鍵函數(shù)來解讀,但不會(huì)成段粘貼源碼。源碼學(xué)習(xí)的效果主要在于自身,所以若對(duì)源碼學(xué)習(xí)感興趣的,可以自行閱讀 Framework 與 Engine 的源碼,也可以閱讀我過往的幾篇文章。

好了,那廢話不多說,直接開始吧!

傳統(tǒng)慣例,只要說到 Flutter 原理的文章,在開頭都會(huì)擺上這張圖。不論講的好不好,都是先擺出來,然后大部分還是靠自行領(lǐng)悟。因?yàn)檫@張圖實(shí)在太好用了。

擺出這張圖,還是簡單從整體上來先認(rèn)識(shí)了一下什么是 Flutter,否則容易陷入“盲人摸象”的境地。

Flutter 架構(gòu)采用分層設(shè)計(jì),從下到上分為三層,依次為:Embedder、Engine、Framework。

  1. Embedder:操作系統(tǒng)適配層,實(shí)現(xiàn)渲染 Surface 設(shè)置、線程設(shè)置等。
  2. Engine:實(shí)現(xiàn) FLutter 渲染引擎、文字排版、事件處理、Dart 運(yùn)行時(shí)等功能。包括了 Skia 圖形繪制庫、Dart VM、Text 等,其中 Skia 和 Text 為上層接口提供了調(diào)用底層渲染和排版的能力。
  3. Framework:是一個(gè)用 Dart 實(shí)現(xiàn)的 UI SDK,從上之下包括了兩大風(fēng)格組件庫、基礎(chǔ)組件庫、圖形繪制、手勢識(shí)別、動(dòng)畫等功能。

至于更多詳情,這張圖配合源碼食用體驗(yàn)會(huì)更好。但由于本文不是源碼解析,所以這個(gè)工作本文就不展開了。接下來,我會(huì)以 Flutter 繪制流程為例,來講解 Flutter 是如何工作的。這也能更好地幫助你理解源碼的思路。

Flutter 繪制流程總結(jié)了一下大體上如下圖所示:

首先是用戶操作,觸發(fā) Widget Tree 的更新,然后構(gòu)建 Element Tree,計(jì)算重繪區(qū)后將信息同步給 RenderObject Tree,之后實(shí)現(xiàn)組件布局、組件繪制、圖層合成、引擎渲染。

作為前置知識(shí),我們先來看看渲染過程中涉及到的數(shù)據(jù)結(jié)構(gòu),再來具體剖析渲染的各個(gè)具體環(huán)節(jié)。

渲染過程中涉及到的關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)包括三棵樹和一個(gè)圖層,其中 RenderObject 持有了 Layer,我們重點(diǎn)先看一下三棵樹之間的關(guān)系。

舉個(gè)栗子,比如有這么一個(gè)簡單的布局:

那么對(duì)應(yīng)的三棵樹之間的關(guān)系如下圖所示:

第一棵樹,是 Widget Tree。它是控件實(shí)現(xiàn)的基本邏輯單位,是用戶對(duì)界面 UI 的描述方式。

需要注意的是,Widget 是不可變的(immutable),當(dāng)視圖配置信息發(fā)生變化時(shí),F(xiàn)lutter 會(huì)重建 Widget 來進(jìn)行更新,以數(shù)據(jù)驅(qū)動(dòng) UI 的方式構(gòu)建簡單高效。

那為什么將 Widget Tree 設(shè)計(jì)為 immutable?Flutter 界面開發(fā)是一種響應(yīng)式編程,主張“simple is fast”,而由上到下重新創(chuàng)建 Widget Tree 來進(jìn)行刷新,這種思路比較簡單,不用額外關(guān)系數(shù)據(jù)更變了會(huì)影響到哪些節(jié)點(diǎn)。另外,Widget 只是一個(gè)配置是數(shù)據(jù)結(jié)構(gòu),創(chuàng)建是輕量的,銷毀也是做過優(yōu)化的,不用擔(dān)心整棵樹重新構(gòu)建帶來的性能問題。

第二棵樹,Element Tree。它是 Widget 的實(shí)例化對(duì)象(如下圖,Widget 提供了 createElement 工廠方法來創(chuàng)建 Element),持久存在于運(yùn)行時(shí)的 Dart 上下文之中。它承載了構(gòu)建的上下文數(shù)據(jù),是連接結(jié)構(gòu)化的配置信息到最終完成渲染的橋梁。

之所以讓它持久地存在于 Dart 上下文中而不是像 Widget 重新構(gòu)建,**因?yàn)?Element Tree 的重新創(chuàng)建和重新渲染的開銷會(huì)非常大,**所以 Element Tree 到 RenderObject Tree 也有一個(gè) Diff 環(huán)節(jié),來計(jì)算最小重繪區(qū)域。

@immutable
abstract class Widget extends DiagnosticableTree {

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

  /// ... 省略其他代碼
}

需要注意的是,Element 同時(shí)持有 Widget 和 RenderObject,但無論是 Widget 還是 Element,其實(shí)都不負(fù)責(zé)最后的渲染,它們只是“發(fā)號(hào)施令”,真正對(duì)配置信息進(jìn)行渲染的是 RenderObject。

第三棵樹,RenderObject Tree,即渲染對(duì)象樹。RenderObject 由 Element 創(chuàng)建并關(guān)聯(lián)到 Element.renderObject 上(如下圖),它接受 Element 的信息同步,同樣的,它也是持久地存在 Dart Runtime 的上下文中,是主要負(fù)責(zé)實(shí)現(xiàn)視圖渲染的對(duì)象。

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 的展示過程分為四個(gè)階段:

  1. 布局
  2. 繪制
  3. 合成
  4. 渲染

其中,布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機(jī)制遍歷渲染對(duì)象樹,確定樹中各個(gè)對(duì)象的位置和尺寸,并把它們繪制到不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 處理。

那么問題來了,為什么是三棵樹而不是兩棵?為什么需要中間的 Element Tree,由 Widget Tree 直接構(gòu)建 RenderObject Tree 不可以嗎?

理論上可以,但實(shí)際不可行。因?yàn)槿绻苯訕?gòu)建 RenderObject Tree 會(huì)極大地增加渲染帶來的性能損耗。因?yàn)?Widget Tree 是不可變的,但 Element 卻是可變的。**實(shí)際上,Element 這一層將 Widget 樹的變化做了抽象(類似 React / Vue 的 VDOM Diff),只將真正需要修改的部分同步到 RenderObject Tree 中,由此最大程度去降低重繪區(qū)域,提高渲染效率。**可以發(fā)現(xiàn),F(xiàn)lutter 的思想很大程度上是借鑒了前端響應(yīng)式框架 React / Vue。

此外,再擴(kuò)展補(bǔ)充一下 VDOM。我們知道,Virtual DOM 的幾個(gè)優(yōu)勢是:

  1. Diff 算法,保證操作盡可能少的 DOM 節(jié)點(diǎn)。這里在 Flutter 的 Element Tree 中體現(xiàn)了出來。
  2. UI 聲明式編程,代碼可維護(hù)性強(qiáng)。這一點(diǎn)在 Dart 聲明式編寫 UI 組件的時(shí)候可以體現(xiàn)出來。
  3. 將真實(shí)的節(jié)點(diǎn)抽象出來,可以方便實(shí)現(xiàn)跨平臺(tái)。這一點(diǎn)在 Flutter 側(cè)沒有體現(xiàn),因?yàn)?Flutter 本身就是跨端的自繪引擎。但換個(gè)思路,我們構(gòu)建 Element 的 Widget Tree 能否不用 Dart 構(gòu)建,專用其他支持運(yùn)行時(shí)編譯的語言構(gòu)建(如 JavaScript),那這樣不就可以實(shí)現(xiàn)動(dòng)態(tài)化了嗎?是的,目前 MXFlutter 就是以這種思路來實(shí)現(xiàn)動(dòng)態(tài)化的。

最后,看看 Layer,它依附于 RenderObject(通過 RenderObject.layer 獲?。抢L圖操作的載體,也可以緩存繪圖操作的結(jié)果。Flutter 分別在不用的圖層上繪圖,然后將這些緩存了繪圖結(jié)果的圖層按照規(guī)則進(jìn)行疊加,得到最終的渲染結(jié)果,也就是我們所說的圖像。

/// src/rendering/layer.dart

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

如上圖代碼所示,Layer 的基類上有兩個(gè)屬性 _needsAddToScene 和 _subtreeNeedsAddToScene,前者表示需要加入場景,后者表示子樹需要加入場景。通常,只有狀態(tài)發(fā)生了更新,才需要加入到場景,所以這兩個(gè)屬性又可以直觀理解為「自己需要更新」和「子樹需要更新」。

Layer 提供了 markNeedsAddToScene() 來把自己標(biāo)記為「需要更新」。派生類在自己狀態(tài)發(fā)生變化時(shí)調(diào)用此方法把自己標(biāo)記為「需要更新」,比如 ContainerLayer 的子節(jié)點(diǎn)增刪、OpacityLayer 的透明度發(fā)生變化、PictureLayer 的 picture 發(fā)生變化等等。

繪制流程分為以下六個(gè)階段:

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

拋開 Diff 和 Render 我們本文不講解,因?yàn)檫@兩部分稍稍繁瑣一些,我們來關(guān)注下剩下的四個(gè)環(huán)節(jié)。

執(zhí)行 build 方法時(shí),根據(jù)組件的類型,存在兩種不同的邏輯。

我們知道,F(xiàn)lutter 內(nèi)的 Widget 可以分為 StatelessWidget 與 StatefulWidget,即無狀態(tài)組件與有狀態(tài)組件。

所謂 StatelessWidget,就是它 build 的信息完全由配置參數(shù)(入?yún)ⅲ┙M成,換句話說,它們一旦創(chuàng)建成功就不再關(guān)心、也不響應(yīng)任何數(shù)據(jù)變化進(jìn)行重繪。

所謂 StatefulWidget,除了父組件初始化時(shí)傳入的靜態(tài)配置之外,還要處理用戶的交互與內(nèi)部數(shù)據(jù)變化(如網(wǎng)絡(luò)數(shù)據(jù)回包)并體現(xiàn)在 UI 上,這類組件就需要以 State 類打來 Widget 構(gòu)建的設(shè)計(jì)方式來實(shí)現(xiàn)。它由 State 的 build 方法構(gòu)建 UI, 最終調(diào)用 buildScope 方法。其會(huì)遍歷 _dirtyElements,對(duì)其調(diào)用 rebuild/build。

只有布局類 Widget 會(huì)觸發(fā) layout(如 Container、Padding、Align 等)。

每個(gè) RenderObject 節(jié)點(diǎn)需要做兩件事:

  1. 調(diào)用自己的 performLayout 來計(jì)算 layout
  2. 調(diào)用 child 的 layout,把 parent 的限制傳入
/// 實(shí)際計(jì)算 layout 的實(shí)現(xiàn)
void performLayout() {
  _size = configuration.size;
  if (child != null) {
    child.layout(BoxConstraints.tight(_size));
  }
}

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  /// ...省略無關(guān)邏輯
  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();
}

如此遞歸一輪,每個(gè)節(jié)點(diǎn)都受到父節(jié)點(diǎn)的約束并計(jì)算出自己的 size,然后父節(jié)點(diǎn)就可以按照自己的邏輯決定各個(gè)子節(jié)點(diǎn)的位置,從而完成整個(gè) Layout 環(huán)節(jié)。

渲染管道中首先找出需要重繪的 RenderObject,如果有實(shí)現(xiàn)了 CustomPainter 則調(diào)用 CustomPainter paint 方法 再調(diào)用 child 的 paint 方法;如果未實(shí)現(xiàn) CustomPainter,則直接調(diào)用 child 的 paint。

在調(diào)用 paint 的時(shí)候,經(jīng)過一串的轉(zhuǎn)換后,layer->PaintingContext->Canvas,最終 paint 就是描繪在 Canvas 上。

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

// 在父類的 paint 里面繼續(xù)調(diào)用 child 的 paint,實(shí)現(xiàn)父子遍歷
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);
  // 在調(diào)用 paint 的時(shí)候,經(jīng)過一串的轉(zhuǎn)換后,layer->PaintingContext->Canvas,最終 paint 就是描繪在 Canvas 上
  painter.paint(canvas, size); 
  /// ...
  canvas.restore();
}

合成主要做三件事情:

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

void compositeFrame() {
  // 省略計(jì)時(shí)邏輯
  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;
  }
}

跨端開發(fā)是必然趨勢,從本質(zhì)上來說,它增加業(yè)務(wù)代碼的復(fù)用率,減少因?yàn)檫m配不同平臺(tái)帶來的工作量,從而降低開發(fā)成本。在各平臺(tái)差異抹平之前,要想“多快好省”地開發(fā)出各端體驗(yàn)接近一致的程序,那便是跨端開發(fā)了。

總得來說,業(yè)內(nèi)普遍認(rèn)同跨端方案存在以下三種:

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

下面來一一講解。

所謂 Web 容器,即是基于 Web 相關(guān)技術(shù)通過瀏覽器組件來實(shí)現(xiàn)界面和功能,包括我們通常意義上說的基于 WebView 的 “H5”、Cordova、Ionic、微信小程序。

這類 Hybrid 開發(fā)模式,只需要將開發(fā)一次 Web,就可以同時(shí)在多個(gè)系統(tǒng)的瀏覽器組件中運(yùn)行,保持基本一致的體驗(yàn),是迄今為止熱度很高的跨端開發(fā)模式。而 Web 與 原生系統(tǒng)之間的通信,則通過 JSBridge 來完成,原生系統(tǒng)通過 JSBridge 接口暴露能力給 Web 調(diào)用。而頁面的呈現(xiàn),則由瀏覽器組件按照標(biāo)準(zhǔn)的瀏覽器渲染流程自行將 Web 加載、解析、渲染。

這類方案的優(yōu)點(diǎn):簡單、天然支持熱更新、生態(tài)繁榮、兼容性強(qiáng)、開發(fā)體驗(yàn)友好。

當(dāng)然,缺點(diǎn)也很明顯,否則就沒有后面兩個(gè)方案什么事了,主要是體驗(yàn)上的問題:

  1. 瀏覽器渲染流程復(fù)雜,頁面需要在線加載,體驗(yàn)受限于網(wǎng)絡(luò)。所以 Web 存在白屏?xí)r間(PWA 例外)、且交互上體驗(yàn)上與原生體驗(yàn)有著非常非常明顯區(qū)別。
  2. 雙端需要分別實(shí)現(xiàn) JSBridge 接口,且 JSBridge 的通信效率一般。

所以輪到泛 Web 容器方案出場了,代表性框架是 React Native,Weex,Hippy。

  • 它放棄了瀏覽器渲染,而采用原生控件,從而保證交互體驗(yàn);
  • 它支持內(nèi)置離線包,來規(guī)避加載耗時(shí)避免長時(shí)間白屏;
  • 它依然采用前端友好的 JavaScript 語言,來保證開發(fā)體驗(yàn)。

在跨端通信上,React Native 依然通過 Bridge 的方式來調(diào)用原生提供的方法。

這套方案理想是美好的,但現(xiàn)實(shí)確實(shí)骨感的,它在實(shí)踐下來之后也依然發(fā)現(xiàn)了問題:

  1. 直接調(diào)用原生控件雖然提升了體驗(yàn)和性能,但是不同端相同的原生控件的渲染結(jié)果是存在差異的,跨端的差異需要巨大的工作量來抹平。
  2. Bridge 的通信效率一般,在需要高頻通信的場景下會(huì)造成丟幀。

那我們究竟能不能既簡單地抹平差異,又同時(shí)保證性能呢?

答案是可以,那就是自繪引擎。不調(diào)用原生控件,我們自己去畫。那就是 Flutter。好比警察問 React Native 嫌疑犯長什么樣子,React Native 只能繪聲繪色地去描繪嫌疑犯的外觀,警察畫完之后再拿給 React Native 看,React Native 還要回答像不像;但 Flutter 自己就是一個(gè)素描大師,它可以自己將嫌疑犯的畫像畫好然后交給警察看。這兩者的效率和表現(xiàn)差異,不言而喻。

  1. 其通過 Skia 圖形庫直接調(diào)用 OpenGL 渲染,保證渲染的高性能,同時(shí)抹平差異性。
  2. 開發(fā)語言選擇同時(shí)支持 JIT 和 AOT 的 Dart,保證開發(fā)效率的同時(shí),較 JavaScript 而言,更是提升了數(shù)十倍的執(zhí)行效率。

通過這樣的思路,F(xiàn)lutter 可以盡可能地減少不同平臺(tái)之間的差異, 同時(shí)保持和原生開發(fā)一樣的高性能。并且對(duì)于系統(tǒng)能力,可以通過開發(fā) Plugin 來支持 Flutter 項(xiàng)目間的復(fù)用。所以說,F(xiàn)lutter 成了三類跨端方案中最靈活的那個(gè),也成了目前業(yè)內(nèi)受到關(guān)注的框架。

flutter 游戲開發(fā)(Flutter 核心原理與混合開發(fā)模式)

至于通信效率,F(xiàn)luter 跨端的通信效率也是高出 JSBridge 許許多多。Flutter 通過 Channel 進(jìn)行通信,其中:

  1. BasicMessageChannel,用于傳遞字符串和半結(jié)構(gòu)化的信息,是全雙工的,可以雙向請(qǐng)求數(shù)據(jù)。
  2. MethodChannel,用于傳遞方案調(diào)用,即 Dart 側(cè)可以調(diào)用原生側(cè)的方法并通過 Result 接口回調(diào)結(jié)果數(shù)據(jù)。
  3. EventChannel:用戶數(shù)據(jù)流的通信,即 Dart 側(cè)監(jiān)聽原生側(cè)的實(shí)時(shí)消息,一旦原生側(cè)產(chǎn)生了數(shù)據(jù),立即回調(diào)給 Dart 側(cè)。

其中,MethodChannel 在開發(fā)中用的比較多,下圖是一個(gè)標(biāo)準(zhǔn)的 MethodChannel 的調(diào)用原理圖:

但為什么我們說 Channel 的性能高呢?梳理一下 MethodChannel 調(diào)用時(shí)的調(diào)用棧,如下圖所示:

可以發(fā)現(xiàn),整個(gè)流程中都是機(jī)器碼的傳遞,而 JNI 的通信又和 JavaVM 內(nèi)部通信效率一樣,整個(gè)流程通信的流程相當(dāng)于原生端的內(nèi)部通信。但是也存在瓶頸。我們可以發(fā)現(xiàn),methodCall 需要編解碼,其實(shí)主要的消耗都在編解碼上了,因此,MethodChannel 并不適合傳遞大規(guī)模的數(shù)據(jù)。

比如我們想調(diào)用攝像頭來拍照或錄視頻,但在拍照和錄視頻的過程中我們需要將預(yù)覽畫面顯示到我們的 Flutter UI中,如果我們要用 MethodChannel 來實(shí)現(xiàn)這個(gè)功能,就需要將攝像頭采集的每一幀圖片都要從原生傳遞到 Dart 側(cè)中,這樣做代價(jià)將會(huì)非常大,因?yàn)閷D像或視頻數(shù)據(jù)通過消息通道實(shí)時(shí)傳輸必然會(huì)引起內(nèi)存和 CPU 的巨大消耗。為此,F(xiàn)lutter 提供了一種基于 Texture 的圖片數(shù)據(jù)共享機(jī)制。

Texture 和 PlatformView 不在本文的探討范圍內(nèi),這里就不再深入展開了,有興趣的讀者可以自行查閱相關(guān)資料作為擴(kuò)展知識(shí)了解。

那接下來,我們就進(jìn)入本文的第三篇章吧,F(xiàn)lutter 混合開發(fā)模式的探索。

Flutter 混合工程的結(jié)構(gòu),主要存在以下兩種模式:

  1. 統(tǒng)一管理模式
  2. 三端分離模式

所謂統(tǒng)一管理模式,就是一個(gè)標(biāo)準(zhǔn)的 Flutter Application 工程,而其中 Flutter 的產(chǎn)物工程目錄(ios/ 和 android/ )是可以進(jìn)行原生混編的工程,如 React Native 進(jìn)行混合開發(fā)那般,在工程項(xiàng)目中進(jìn)行混合開發(fā)就好。但是這樣的缺點(diǎn)是當(dāng)原生項(xiàng)目業(yè)務(wù)龐大起來時(shí),F(xiàn)lutter 工程對(duì)于原生工程的耦合就會(huì)非常嚴(yán)重,當(dāng)工程進(jìn)行升級(jí)時(shí)會(huì)比較麻煩。因此這種混合模式只適用于 Flutter 業(yè)務(wù)主導(dǎo)、原生功能為輔的項(xiàng)目。但早期 Google 未支持 Flutter Module 時(shí),進(jìn)行混合開發(fā)也只存在這一種模式。

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

為了問題的簡潔性,我們這里暫時(shí)不考慮生命周期的統(tǒng)一性和通信層的實(shí)現(xiàn),而除此之外,混合導(dǎo)航棧主要需要解決以下四種場景下的問題:

  1. Native 跳轉(zhuǎn) Flutter
  2. Flutter 跳轉(zhuǎn) Flutter
  3. Flutter 跳轉(zhuǎn) Native
  4. Native 跳轉(zhuǎn) Native

Native -> Flutter,這種情況比較簡單,F(xiàn)lutter Engine 已經(jīng)為我們提供了現(xiàn)成的 Plugin,即 iOS 下的 FlutterViewController 與 Android 下的 FlutterView(自行包裝一下可以實(shí)現(xiàn) FlutterActivity),所以這種場景我們直接使用啟動(dòng)了的 Flutter Engine 來初始化 Flutter 容器,為其設(shè)置初始路由頁面之后,就可以以原生的方式跳轉(zhuǎn)至 Flutter 頁面了。

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

Flutter -> Flutter,業(yè)內(nèi)存在兩種方案,后續(xù)我們會(huì)詳細(xì)介紹到,分別是:

  1. 使用 Flutter 本身的 Navigator 導(dǎo)航棧
  2. 創(chuàng)建新的 Flutter 容器后,使用原生導(dǎo)航棧

Flutter -> Native,需要注意的時(shí),這里的跳轉(zhuǎn)其實(shí)是包含了兩種情況:

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

如上圖,這種情況相對(duì)復(fù)雜,我們需要使用 MethodChannel 讓 Dart 與 Platform 側(cè)進(jìn)行通信,Dart 發(fā)出 open 或 close 的指令后由原生側(cè)執(zhí)行相應(yīng)的邏輯。

Native -> Native,這種情況沒有什么好說的,直接使用原生的導(dǎo)航棧即可。

為了解決混合棧問題,以及彌補(bǔ) Flutter 自身對(duì)混合開發(fā)支持的不足,業(yè)內(nèi)提出了一些混合??蚣?,總得來說,離不開這四種混合模式:

  1. Flutter Boost 為代表的類 WebView 導(dǎo)航棧
  2. Flutter Thrio 為代表的 Navigator 導(dǎo)航棧
  3. 多 Engine 混合模式
  4. View 級(jí)別的混合模式

下面,一一來談?wù)勊鼈兊脑砼c優(yōu)缺點(diǎn)。

Flutter Boost 是閑魚團(tuán)隊(duì)開源的 Flutter 混合框架,成熟穩(wěn)定,業(yè)內(nèi)影響力高,在導(dǎo)航棧的處理思路上沒有繞開我們?cè)?3.2 節(jié)中談及的混合棧原理,但需要注意的是,當(dāng) Flutter 跳轉(zhuǎn) Flutter 時(shí),它采用的是 new 一個(gè)新的 FlutterViewController 后使用原生導(dǎo)航棧跳轉(zhuǎn)的方式,如下圖所示:

這么做的好處是使用者(業(yè)務(wù)開發(fā)者)操作 Flutter 容器就如同操作 WebView 一樣,而 Flutter 頁面就如同 Web 頁面,邏輯上簡單清晰,將所有的導(dǎo)航路由邏輯收歸到原生側(cè)處理。如下圖,是調(diào)用 open 方法時(shí) Flutter Boost 的時(shí)序圖(關(guān)鍵函數(shù)路徑),這里可以看到兩點(diǎn)信息:

  1. 混合導(dǎo)航棧的邏輯主要包括原生層、通信層、Dart 層。
  2. Flutter Boost 的 open 方法實(shí)現(xiàn)邏輯相對(duì)簡單。

但是它也有缺點(diǎn),就是每次打開 Flutter 頁面都需要 new 一個(gè) ViewController,在連續(xù)的 Flutter 跳轉(zhuǎn) Flutter 的場景下有額外的內(nèi)存開銷。針對(duì)這個(gè)問題,又有團(tuán)隊(duì)開發(fā)了 Flutter Thrio。

上面我們說到,F(xiàn)lutter 跳轉(zhuǎn) Flutter 這種場景 Flutter Boost 存在額外的內(nèi)存開銷,故哈啰出行團(tuán)隊(duì)今年4月開源了 Flutter Thrio 混合框架,其針對(duì) Flutter Boost 做出的最重要的改變?cè)谟冢篎lutter 跳轉(zhuǎn) Flutter 這種場景下,Thrio 使用了 Flutter Navigator 導(dǎo)航棧。如下圖所示:

在連續(xù)的 Flutter 頁面跳轉(zhuǎn)場景下,內(nèi)存測試圖表如下:

從這張圖表中我們可以得到以下幾點(diǎn)信息:

  1. 紅色區(qū)域是啟動(dòng) Flutter Engine 的內(nèi)存增量,基本接近 30MB,F(xiàn)lutter Engine 是一個(gè)比較重的對(duì)象。
  2. FlutterViewController 帶來的內(nèi)存增量普遍在 12~15MB 左右。

可見,在這種場景下,Thrio 還是做出了一定的優(yōu)化的。但與之帶來的,就是實(shí)現(xiàn)的復(fù)雜性。我們談到 Flutter Boost 的優(yōu)點(diǎn)是簡單,路由全部收歸原生導(dǎo)航棧。而 Flutter Thrio 混用了原生導(dǎo)航棧和 Flutter Navigator,因此實(shí)現(xiàn)會(huì)相對(duì)更復(fù)雜一下。這里我梳理了一下 Flutter Thrio open 時(shí)關(guān)鍵函數(shù)路徑,可以看到,Thrio 的導(dǎo)航管理確實(shí)是復(fù)雜了一些。

以上我們談及的兩種混合框架都是單引擎的,對(duì)應(yīng)的,也存在多引擎的框架。在談多引擎之前,還是需要先介紹一下關(guān)于 Engine、Dart VM、isolate 幾個(gè)前置知識(shí)點(diǎn)。

在第一篇章中我們沒有涉及到 Engine 層的源碼分析,而著重篇幅去講解 Framework 層的原理,一是為了第一章的連貫性,二是此處也會(huì)單獨(dú)說到 Engine,還是最好放在此時(shí)講解會(huì)更便于記憶與理解。

Dart VM、Engine 與 isolate

(a)Dart 虛擬機(jī)創(chuàng)建完成之后,需要?jiǎng)?chuàng)建 Engine 對(duì)象,然后會(huì)調(diào)用
DartIsolate::CreateRootIsolate() 來創(chuàng)建 isolate。 (b)每一個(gè) Engine 實(shí)例都為 UI、GPU、IO、Platform Runner 創(chuàng)建各自新的 Thread。 (c)isolate,顧名思義,內(nèi)存在邏輯上是隔離的。 (d)isolate 中的 code 是按順序執(zhí)行的,任何 Dart 程序的并發(fā)都是運(yùn)行多個(gè) isolate 的結(jié)果。當(dāng)然我們可以開啟多個(gè) isolate 來處理 CPU 密集型任務(wù)。

根據(jù)(a)我們可以推出:(1) 每個(gè) Engine 對(duì)應(yīng)一個(gè) isolate 對(duì)象,即 Root Isolate。 根據(jù)(b)我們可以推出:(2) Engine 是一個(gè)比較重的對(duì)象(前文也有所提及)。 根據(jù)(c)和 (1) 我們可以推出:(3) Engine 與 Engine 之間相互隔離。 根據(jù)(d)和 (3) 我們可以推出:(4) Engine 沒有共享內(nèi)存的并發(fā),沒有競爭的可能性,不需要鎖,也就不存在死鎖問題。

好啦,記住這四個(gè)結(jié)論,我們?cè)賮砜纯?window。

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

從類的定義上來看,window 是連接 Framework 與 Engine 的窗口。在 Framework 層,window 指的是 ui.window 單例對(duì)象,源碼文件是 window.dart。而在 Engine 層,源碼文件是 window.cc,兩者交互的 API 很少,但是一一對(duì)應(yīng):

可以發(fā)現(xiàn),這些主要是 Framework 層調(diào)用 Engine 層中 Skia 庫封裝后的相關(guān) API。那就不得不說說它的第二層含義——作為繪圖的窗口。

從功能上來看,在界面繪制交互意義上,window 也是繪圖的窗口。在 Engine 中,繪圖操作輸出到了一個(gè) PictureRecorder 的對(duì)象上;在此對(duì)象上調(diào)用 endRecording() 得到一個(gè) Picture 對(duì)象,然后需要在合適的時(shí)候把 Picture 對(duì)象添加(add)到 SceneBuilder 對(duì)象上;調(diào)用 SceneBuilder 對(duì)象的 build() 方法獲得一個(gè) Scene 對(duì)象;最后,在合適的時(shí)機(jī)把 Scene 對(duì)象傳遞給 window.render() 方法,最終把場景渲染出來。

實(shí)例代碼如下:

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 模式

綜上,根據(jù)(1)(3)(5)我們可以得出下圖的多引擎模式:

它有以下幾個(gè)特征:

  1. App 內(nèi)存在多個(gè)引擎
  2. 每個(gè)引擎內(nèi)有若干個(gè) FlutterVC
  3. Engine 與 Engine 之間是隔離的

根據(jù)這三個(gè)特征,我們可以設(shè)想一下其通信層的實(shí)現(xiàn),假設(shè)存在兩個(gè)引擎,每個(gè)引擎內(nèi)又存在兩個(gè) FlutterVC,每個(gè) FlutterVC 內(nèi)又存在兩個(gè) Flutter 頁面,那這種場景下的跳轉(zhuǎn)就會(huì)變得非常復(fù)雜(下圖出自 Thrio 開源倉庫中的README):

所以顯而易見的,我們不可否認(rèn) Engine 之間的邏輯隔離帶來了模塊間天然的隔離性,但是問題也有許多:

首先如上圖所示,通信層設(shè)計(jì)會(huì)異常復(fù)雜,而且通信層的核心邏輯依然是需要放在原生側(cè)來實(shí)現(xiàn),如此便一定程度上失去了跨端開發(fā)的優(yōu)勢。

其次,我們反復(fù)提到 Engine 是一個(gè)比較重的對(duì)象,啟動(dòng)多個(gè) Flutter Engine 會(huì)導(dǎo)致資源消耗過多。

最后,由于 Engine 之間沒有共享內(nèi)存,這種天然的隔離性其實(shí)弊大于利,在混合開發(fā)的視角下,一個(gè) App 需要維護(hù)兩套緩存池——原生緩存池與 DartVM 所持有的緩存池,但是隨著開啟多 Engine 的介入,后者緩存池的資源又互不相通,導(dǎo)致資源開銷變得更加巨大。

為了解決傳統(tǒng)的多 Engine 模式所帶來的這些問題,又有團(tuán)隊(duì)提出了基于 View 級(jí)別的混合模式。

基于 View 級(jí)別的混合模式,核心是為每個(gè) window 加入 windowId 的概念,以便它們?nèi)ス蚕硗环?Root Isolate。我們剛才說到,一個(gè) isolate 具有一個(gè) ui.window 單例對(duì)象,那么只需要做一點(diǎn)修改,把 Flutter Engine 加入 ID 的概念傳給 Dart 層,讓 Dart 層存在多個(gè) window,就可以實(shí)現(xiàn)多個(gè) Flutter Engine 共享一個(gè) isolate 了。

如下圖所示:

這樣就可以真正實(shí)現(xiàn) View 級(jí)別的混合開發(fā),可以同時(shí)持有多份 FlutterViewController,且這些 FlutterVC 可以內(nèi)存共享。

那缺點(diǎn)也比較明顯,我們需要對(duì) Engine 代碼做出修改,維護(hù)成本會(huì)很高。其次,多 Engine 的資源消耗問題在這種模式下也是需要通過對(duì) Engine 不斷裁剪來解決的。

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

所謂 JIT,Just In Time,即時(shí)編譯/運(yùn)行時(shí)編譯,在 Debug 模式中使用,可以動(dòng)態(tài)下發(fā)和執(zhí)行代碼,但是執(zhí)行性能受運(yùn)行時(shí)編譯影響。

所謂 AOT,Ahead Of Time,提前編譯/運(yùn)行前編譯,在 Release 模式中使用,可以為特定平臺(tái)生成二進(jìn)制代碼,執(zhí)行性能好、運(yùn)行速度快,但每次執(zhí)行都需要提前編譯,開發(fā)調(diào)試效率低。

對(duì)應(yīng)的 Flutter App 存在三種運(yùn)行模式:

  • Debug
  • Release
  • Profile

因此,我們可以看出,在開發(fā)調(diào)試過程中,我們需要使用支持 JIT 的 Debug 模式,而在生產(chǎn)環(huán)境中,我們需要構(gòu)建包為支持 AOT 的 Release 模式以保證性能。

那么,這對(duì)我們的集成與構(gòu)建也提出了一定的要求。

所謂集成,指的是混合項(xiàng)目中,將 Flutter Module 的產(chǎn)物集成到原生項(xiàng)目中去,存在兩種集成方式,區(qū)別如下:

可以發(fā)現(xiàn)源碼集成是 Flutter dev 分支需要的,但是產(chǎn)物集成是 Flutter dev 以外的分支需要的。在這里,我們的混合項(xiàng)目需要同時(shí)支持兩種不同的集成工程,在 Flutter dev 分支上進(jìn)行源碼集成開發(fā),然后依賴抽取構(gòu)建產(chǎn)物發(fā)布到遠(yuǎn)程,如 iOS 構(gòu)建成 pods 發(fā)布到 Cocoapods 對(duì)應(yīng)的倉庫,而 Android 構(gòu)建成 AAR 發(fā)布到 Maven 對(duì)應(yīng)的云端。于是,其他分支的工程直接 gradle 或者 pod install 就可以更新 Flutter 依賴模塊了。

當(dāng)然,我們說到運(yùn)行模式存在 Debug、Release、Profile 三種,其對(duì)應(yīng)的集成產(chǎn)物也會(huì)區(qū)分這三種版本,但由于產(chǎn)物集成無法調(diào)試,集成 Debug 版本和 Profile 版本沒有意義,因此依賴抽取發(fā)布時(shí)只需要發(fā)布 Release 版本的產(chǎn)物就好。

在整套「Fan 直播」Flutter 混合項(xiàng)目搭建之后,我們形成了一套初具雛形的 Flutter 工作流。在未來,我們也會(huì)不斷完善 Flutter 混合開發(fā)模式,積極參與到 Flutter 的生態(tài)建設(shè)中去。

flutter 游戲開發(fā)(Flutter 核心原理與混合開發(fā)模式)

熱門課程推薦

熱門資訊

請(qǐng)綁定手機(jī)號(hào)

x

同學(xué)您好!

您已成功報(bào)名0元試學(xué)活動(dòng),老師會(huì)在第一時(shí)間與您取得聯(lián)系,請(qǐng)保持電話暢通!
確定