在「Fan 直播」的 Flutter 混合開發實踐中,我們總結了一些 Flutter 混合開發的經驗特此分享。本文將從 Flutter 原理出發,詳細介紹 Flutter 的繪製原理,藉由此來對比三種跨端方案;之後再進入第三篇章 Flutter 混合開發模式的講解,主要是四種不同的 Flutter 混合模式的原理分析;最後簡單分享一下混合工程的工程化探索。
“唯有深入,方能淺出”,對於一門技術,只有了解的深入,才能用最淺顯、通俗的話語描述出。在此之前,我寫過一些 Flutter 的文章,但性質更偏向於學習筆記與源碼閱讀筆記,因此較為晦澀,且零碎繁亂。本文作為階段性的總結,我儘可能以淺顯易懂的文字、循序漸進地來分享 Flutter 混合開發的知識,對於關鍵內容會輔以源碼或源碼中的關鍵函數來解讀,但不會成段粘貼源碼。源碼學習的效果主要在於自身,所以若對源碼學習感興趣的,可以自行閱讀 Framework 與 Engine 的源碼,也可以閱讀我過往的幾篇文章。
好了,那廢話不多說,直接開始吧!
1. Flutter 核心原理#
1.1 Flutter 架構#
注:此圖引自 Flutter System Overview
傳統慣例,只要說到 Flutter 原理的文章,在開頭都會擺上這張圖。不論講得好不好,都是先擺出來,然後大部分還是靠自行領悟。因為這張圖實在太好用了。
擺出這張圖,還是簡單從整體上來先認識了一下什麼是 Flutter,否則容易陷入 “盲人摸象” 的境地。
Flutter 架構採用分層設計,從下到上分為三層,依次為:Embedder、Engine、Framework。
- Embedder:操作系統適配層,實現渲染 Surface 設置、線程設置等。
- Engine:實現 Flutter 渲染引擎、文字排版、事件處理、Dart 運行時等功能。包括了 Skia 圖形繪製庫、Dart VM、Text 等,其中 Skia 和 Text 為上層接口提供了調用底層渲染和排版的能力。
- Framework:是一個用 Dart 實現的 UI SDK,從上之下包括了兩大風格組件庫、基礎組件庫、圖形繪製、手勢識別、動畫等功能。
至於更多詳情,這張圖配合源碼食用體驗會更好。但由於本文不是源碼解析,所以這個工作本文就不展開了。接下來,我會以 Flutter 繪製流程為例,來講解 Flutter 是如何工作的。這也能更好地幫助你理解源碼的思路。
1.2 Flutter 繪製原理#
Flutter 繪製流程總結了一下大體上如下圖所示:
首先是用戶操作,觸發 Widget Tree 的更新,然後構建 Element Tree,計算重繪區後將信息同步給 RenderObject Tree,之後實現組件佈局、組件繪製、圖層合成、引擎渲染。
作為前置知識,我們先來看看渲染過程中涉及到的數據結構,再來具體剖析渲染的各個具體環節。
1.3 Flutter 渲染過程中的數據結構#
渲染過程中涉及到的關鍵的數據結構包括三棵樹和一個圖層,其中 RenderObject 持有了 Layer,我們重點先看一下三棵樹之間的關係。
舉個栗子,比如有這麼一個簡單的佈局:
那麼對應的三棵樹之間的關係如下圖所示:
1.3.1 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。它是 Widget 的實例化對象(如下圖,Widget 提供了 createElement
工廠方法來創建 Element),持久存在於運行時的 Dart 上下文之中。它承載了構建的上下文數據,是連接結構化的配置信息到最終完成渲染的橋梁。
之所以讓它持久地存在於 Dart 上下文中而不是像 Widget 重新構建,** 因為 Element Tree 的重新創建和重新渲染的開銷會非常大,** 所以 Element Tree 到 RenderObject Tree 也有一個 Diff 環節,來計算最小重繪區域。
需要注意的是,Element 同時持有 Widget 和 RenderObject,但無論是 Widget 還是 Element,其實都不負責最後的渲染,它們只是 “發號施令”,真正對配置信息進行渲染的是 RenderObject。
1.3.3 RenderObject Tree#
第三棵樹,RenderObject Tree,即渲染對象樹。RenderObject 由 Element 創建並關聯到 Element.renderObject
上(如下圖),它接受 Element 的信息同步,同樣的,它也是持久地存在 Dart Runtime 的上下文中,是主要負責實現視圖渲染的對象。
RenderObject Tree 在 Flutter 的展示過程分為四個階段:
- 佈局
- 繪製
- 合成
- 渲染
其中,佈局和繪製在 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 的幾個優勢是:
- Diff 算法,保證操作盡可能少的 DOM 節點。這裡在 Flutter 的 Element Tree 中體現了出來。
- UI 聲明式編程,代碼可維護性強。這一點在 Dart 聲明式編寫 UI 組件的時候可以體現出來。
- 將真實的節點抽象出來,可以方便實現跨平台。這一點在 Flutter 端沒有體現,因為 Flutter 本身就是跨端的自繪引擎。但換個思路,我們構建 Element 的 Widget Tree 能否不用 Dart 構建,專用其他支持運行時編譯的語言構建(如 JavaScript),那這樣不就可以實現動態化了嗎?是的,目前 MXFlutter 就是以這種思路來實現動態化的。
1.3.4 Layers#
最後,看看 Layer,它依附於 RenderObject(通過 RenderObject.layer
獲取),是繪圖操作的載體,也可以緩存繪圖操作的結果。Flutter 分別在不用的圖層上繪圖,然後將這些緩存了繪圖結果的圖層按照規則進行疊加,得到最終的渲染結果,也就是我們所說的圖像。
如上圖代碼所示,Layer 的基類上有兩個屬性 _needsAddToScene
和 _subtreeNeedsAddToScene
,前者表示需要加入場景,後者表示子樹需要加入場景。通常,只有狀態發生了更新,才需要加入到場景,所以這兩個屬性又可以直觀理解為「自己需要更新」和「子樹需要更新」。
Layer 提供了 markNeedsAddToScene()
來把自己標記為「需要更新」。派生類在自己狀態發生變化時調用此方法把自己標記為「需要更新」,比如 ContainerLayer 的子節點增刪、OpacityLayer 的透明度發生變化、PictureLayer 的 picture 發生變化等等。
1.4 Flutter 繪製流程拆解#
繪製流程分為以下六個階段:
- Build
- Diff
- Layout
- Paint
- Composite
- Render
拋開 Diff 和 Render 我們本文不講解,因為這兩部分稍稍繁瑣一些,我們來關注下剩下的四個環節。
注:此流程圖出自 複雜業務如何保證 Flutter 的高性能高流暢度?| 閒魚技術,可以較為清晰的表達 Flutter 核心的繪製流程了。
1.4.1 Build#
執行 build 方法時,根據組件的類型,存在兩種不同的邏輯。
我們知道,Flutter 內的 Widget 可以分為 StatelessWidget 與 StatefulWidget,即無狀態組件與有狀態組件。
所謂 StatelessWidget,就是它 build 的信息完全由配置參數(入參)組成,換句話說,它們一旦創建成功就不再關心、也不響應任何數據變化進行重繪。
所謂 StatefulWidget,除了父組件初始化時傳入的靜態配置之外,還要處理用戶的交互與內部數據變化(如網絡數據回包)並體現在 UI 上,這類組件就需要以 State 類打來 Widget 構建的設計方式來實現。它由 State 的 build 方法構建 UI,最終調用 buildScope
方法。其會遍歷 _dirtyElements
,對其調用 rebuild/build。
注:以上兩圖出自 《Flutter 核心技術與實戰 | 陳航》
1.4.2 Layout#
只有佈局類 Widget 會觸發 layout(如 Container、Padding、Align 等)。
每個 RenderObject 節點需要做兩件事:
- 調用自己的 performLayout 來計算 layout
- 調用 child 的 layout,把 parent 的限制傳入
如此遞歸一輪,每個節點都受到父節點的約束並計算出自己的 size,然後父節點就可以按照自己的邏輯決定各個子節點的位置,從而完成整個 Layout 環節。
1.4.3 Paint#
渲染管道中首先找出需要重繪的 RenderObject,如果有實現了 CustomPainter 則調用 CustomPainter paint 方法,再調用 child 的 paint 方法;如果未實現 CustomPainter,則直接調用 child 的 paint。
在調用 paint 的時候,經過一串的轉換後,layer->PaintingContext->Canvas
,最終 paint 就是描繪在 Canvas 上。
1.4.4 Composite#
合成主要做三件事情:
- 把所有 Layer 組合成 Scene
- 通過
ui.window.render
方法,把 Scene 提交給 Engine。 - Engine 把計算所有的 Layer 最終的顯示效果,渲染到屏幕上。
2. 跨端方案對比#
跨端開發是必然趨勢,從本質上來說,它增加業務代碼的復用率,減少因為適配不同平台帶來的工作量,從而降低開發成本。在各平台差異抹平之前,要想 “多快好省” 地開發出各端體驗接近一致的程序,那便是跨端開發了。
總得來說,業內普遍認同跨端方案存在以下三種:
- Web 容器方案
- 泛 Web 容器方案
- 自繪引擎方案
下面來一一講解。
2.1 Web 容器#
所謂 Web 容器,即是基於 Web 相關技術通過瀏覽器組件來實現界面和功能,包括我們通常意義上說的基於 WebView 的 “H5”、Cordova、Ionic、微信小程序。
這類 Hybrid 開發模式,只需要將開發一次 Web,就可以同時在多個系統的瀏覽器組件中運行,保持基本一致的體驗,是迄今為止熱度很高的跨端開發模式。而 Web 與 原生系統之間的通信,則通過 JSBridge 來完成,原生系統通過 JSBridge 接口暴露能力給 Web 調用。而頁面的呈現,則由瀏覽器組件按照標準的瀏覽器渲染流程自行將 Web 加載、解析、渲染。
這類方案的優點:簡單、天然支持熱更新、生態繁榮、兼容性強、開發體驗友好。
當然,缺點也很明顯,否則就沒有後面兩個方案什麼事了,主要是體驗上的問題:
- 瀏覽器渲染流程複雜,頁面需要在線加載,體驗受限於網絡。所以 Web 存在白屏時間(PWA 例外)、且交互上體驗上與原生體驗有著非常非常明顯區別。
- 雙端需要分別實現 JSBridge 接口,且 JSBridge 的通信效率一般。
2.2 泛 Web 容器#
所以輪到泛 Web 容器方案出場了,代表性框架是 React Native,Weex,Hippy。
- 它放棄了瀏覽器渲染,而採用原生控件,從而保證交互體驗;
- 它支持內置離線包,來規避加載耗時避免長時間白屏;
- 它依然採用前端友好的 JavaScript 語言,來保證開發體驗。
在跨端通信上,React Native 依然通過 Bridge 的方式來調用原生提供的方法。
這套方案理想是美好的,但現實確實骨感的,它在實踐下來之後也依然發現了問題:
- 直接調用原生控件雖然提升了體驗和性能,但是不同端相同的原生控件的渲染結果是存在差異的,跨端的差異需要巨大的工作量來抹平。
- Bridge 的通信效率一般,在需要高頻通信的場景下會造成丟幀。
2.3 自繪引擎#
那我們究竟能不能既簡單地抹平差異,又同時保證性能呢?
答案是可以,那就是自繪引擎。不調用原生控件,我們自己去畫。那就是 Flutter。好比警察問 React Native 嫌疑犯長什麼樣子,React Native 只能繪聲繪色地去描繪嫌疑犯的外觀,警察畫完之後再拿給 React Native 看,React Native 還要回答像不像;但 Flutter 自己就是一個素描大師,它可以自己將嫌疑犯的画像畫好然後交給警察看。這兩者的效率和表現差異,不言而喻。
- 其通過 Skia 圖形庫直接調用 OpenGL 渲染,保證渲染的高性能,同時抹平差異性。
- 開發語言選擇同時支持 JIT 和 AOT 的 Dart,保證開發效率的同時,較 JavaScript 而言,更是提升了數十倍的執行效率。
通過這樣的思路,Flutter 可以儘可能地減少不同平台之間的差異,同時保持和原生開發一樣的高性能。並且對於系統能力,可以通過開發 Plugin 來支持 Flutter 項目間的復用。所以說,Flutter 成了三類跨端方案中最靈活的那個,也成了目前業內受到關注的框架。
至於通信效率,Fluter 跨端的通信效率也是高出 JSBridge 許許多多。Flutter 通過 Channel 進行通信,其中:
- BasicMessageChannel,用於傳遞字符串和半結構化的信息,是全雙工的,可以雙向請求數據。
- MethodChannel,用於傳遞方案調用,即 Dart 端可以調用原生端的方法並通過 Result 接口回調結果數據。
- EventChannel:用戶數據流的通信,即 Dart 端監聽原生端的實時消息,一旦原生端產生了數據,立即回調給 Dart 端。
其中,MethodChannel 在開發中用的比較多,下圖是一個標準的 MethodChannel 的調用原理圖:
但為什麼我們說 Channel 的性能高呢?梳理一下 MethodChannel 調用時的調用棧,如下圖所示:
可以發現,整個流程中都是機器碼的傳遞,而 JNI 的通信又和 JavaVM 內部通信效率一樣,整個流程通信的流程相當於原生端的內部通信。但是也存在瓶頸。我們可以發現,methodCall 需要編解碼,其實主要的消耗都在編解碼上了,因此,MethodChannel 並不適合傳遞大規模的數據。
比如我們想調用攝像頭來拍照或錄視頻,但在拍照和錄視頻的過程中我們需要將預覽畫面顯示到我們的 Flutter UI 中,如果我們要用 MethodChannel 來實現這個功能,就需要將攝像頭採集的每一幀圖片都要從原生傳遞到 Dart 端中,這樣做代價將會非常大,因為將圖像或視頻數據通過消息通道實時傳輸必然會引起內存和 CPU 的巨大消耗。為此,Flutter 提供了一種基於 Texture 的圖片數據共享機制。
Texture 和 PlatformView 不在本文的探討範圍內,這裡就不再深入展開了,有興趣的讀者可以自行查閱相關資料作為擴展知識了解。
那接下來,我們就進入本文的第三篇章吧,Flutter 混合開發模式的探索。
3. Flutter 混合開發模式#
3.1 混合模式#
Flutter 混合工程的結構,主要存在以下兩種模式:
- 統一管理模式
- 三端分離模式
所謂統一管理模式,就是一個標準的 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 混合棧原理#
為了問題的簡潔性,我們這裡暫時不考慮生命週期的統一性和通信層的實現,而除此之外,混合導航棧主要需要解決以下四種場景下的問題:
- Native 跳轉 Flutter
- Flutter 跳轉 Flutter
- Flutter 跳轉 Native
- Native 跳轉 Native
3.2.1 Native 跳轉 Flutter#
Native -> Flutter,這種情況比較簡單,Flutter Engine 已經為我們提供了現成的 Plugin,即 iOS 下的 FlutterViewController 與 Android 下的 FlutterView(自行包裝一下可以實現 FlutterActivity),所以這種場景我們直接使用啟動了的 Flutter Engine 來初始化 Flutter 容器,為其設置初始路由頁面之後,就可以以原生的方式跳轉至 Flutter 頁面了。
3.2.2 Flutter 跳轉 Flutter#
Flutter -> Flutter,業內存在兩種方案,後續我們會詳細介紹到,分別是:
- 使用 Flutter 本身的 Navigator 導航棧
- 創建新的 Flutter 容器後,使用原生導航棧
3.2.3 Flutter 跳轉 Native#
Flutter -> Native,需要注意的是,這裡的跳轉其實是包含了兩種情況,一是打開原生頁面(open,包括但不限於 push),二是回退到原生頁面(close,包括但不限於 pop)。
如上圖,這種情況相對複雜,我們需要使用 MethodChannel 讓 Dart 與 Platform 端進行通信,Dart 發出 open 或 close 的指令後由原生端執行相應的邏輯。
3.2.4 Native 跳轉 Native#
Native -> Native,這種情況沒有什麼好說的,直接使用原生的導航棧即可。
3.3 混合模式#
為了解決混合棧問題,以及彌補 Flutter 自身對混合開發支持的不足,業內提出了一些混合棧框架,總得來說,離不開這四種混合模式:
- Flutter Boost 為代表的類 WebView 導航棧
- Flutter Thrio 為代表的 Navigator 導航棧
- 多 Engine 混合模式
- View 基本的混合模式
下面,一一來談談它們的原理與優缺點。
3.3.1 Flutter Boost#
Flutter Boost 是閒魚團隊開源的 Flutter 混合框架,成熟穩定,業內影響力高,在導航棧的處理思路上沒有繞開我們在 3.2 節中談及的混合棧原理,但需要注意的是,當 Flutter 跳轉 Flutter 時,它採用的是 new 一個新的 FlutterViewController 後使用原生導航棧跳轉的方式,如下圖所示:
這麼做的好處是使用者(業務開發者)操作 Flutter 容器就如同操作 WebView 一樣,而 Flutter 頁面就如同 Web 頁面,邏輯上簡單清晰,將所有的導航路由邏輯收歸到原生端處理。如下圖,是調用 open 方法時 Flutter Boost 的時序圖(關鍵函數路徑),這裡可以看到兩點信息:
- 混合導航棧的邏輯主要包括原生層、通信層、Dart 層。
- 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 頁面跳轉場景下,內存測試圖表如下:
從這張圖表中我們可以得到以下幾點信息:
- 紅色區域是啟動 Flutter Engine 的內存增量,基本接近 30MB,Flutter Engine 是一個比較重的對象。
- FlutterViewController 帶來的內存增量普遍在 12~15MB 左右。
可見,在這種場景下,Thrio 還是做出了一定的優化的。但與之帶來的,就是實現的複雜性。我們談到 Flutter Boost 的優點是簡單,路由全部收歸原生導航棧。而 Flutter Thrio 混用了原生導航棧和 Flutter Navigator,因此實現會相對更複雜一下。這裡我梳理了一下 Flutter Thrio open 時關鍵函數路徑,可以看到,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 很少,但是一一對應:
可以發現,這些主要是 Framework 層調用 Engine 層中 Skia 庫封裝後的相關 API。那就不得不說說它的第二層含義 —— 作為繪圖的窗口。
從功能上來看,在界面繪製交互意義上,window 也是繪圖的窗口。在 Engine 中,繪圖操作輸出了到一個 PictureRecorder
的對象上;在此對象上調用 endRecording()
得到一個 Picture
對象,然後需要在合適的時候把 Picture
對象添加(add)到 SceneBuilder
對象上;調用 SceneBuilder
對象的 build()
方法獲得一個 Scene
對象;最後,在合適的時機把 Scene
對象傳遞給 window.render()
方法,最終把場景渲染出來。
實例代碼如下:
多 Engine 模式#
綜上,根據(1)(3)(5)我們可以得出下圖的多引擎模式:
它有以下幾個特徵:
- App 內存在多個引擎
- 每個引擎內有若干個 FlutterVC
- Engine 與 Engine 之間是隔離的
根據這三個特徵,我們可以設想一下其通信層的實現,假設存在兩個引擎,每個引擎內又存在兩個 FlutterVC,每個 FlutterVC 內又存在兩個 Flutter 頁面,那這種場景下的跳轉就會變得非常複雜(下圖出自 Thrio 開源倉庫中的 README):
所以顯而易見的,我們不可否認 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 級別的混合開發,可以同時持有多份 FlutterViewController,且這些 FlutterVC 可以內存共享。
那缺點也比較明顯,我們需要對 Engine 代碼做出修改,維護成本會很高。其次,多 Engine 的資源消耗問題在這種模式下也是需要通過對 Engine 不斷裁剪來解決的。
4. 工程化探索#
4.1 編譯模式#
Dart 天然支持兩種編譯模式,JIT 與 AOT。
4.1.1 JIT 與 AOT#
所謂 JIT,Just In Time,即時編譯 / 運行時編譯,在 Debug 模式中使用,可以動態下發和執行代碼,但是執行性能受運行時編譯影響。
所謂 AOT,Ahead Of Time,提前編譯 / 運行前編譯,在 Release 模式中使用,可以為特定平台生成二進制代碼,執行性能好、運行速度快,但每次執行都需要提前編譯,開發調試效率低。
4.1.2 Debug、Release、Profile#
對應的 Flutter App 存在三種運行模式:
- Debug
- Release
- Profile
因此,我們可以看出,在開發調試過程中,我們需要使用支持 JIT 的 Debug 模式,而在生產環境中,我們需要構建包為支持 AOT 的 Release 模式以保證性能。
那麼,這對我們的集成與構建也提出了一定的要求。
4.2 集成與構建#
所謂集成,指的是混合項目中,將 Flutter Module 的產物集成到原生項目中去,存在兩種集成方式,區別如下:
可以發現源碼集成是 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 的生態建設中去。