一直以來,跨平臺開發都是困擾移動客戶端開發的難題。前端
在馬蜂窩旅遊 App 不少業務場景裏,咱們嘗試過一些主流的跨平臺開發解決方案,好比 WebView 和 React Native,來提高開發效率和用戶體驗。但這兩種方式也帶來了新的問題。android
好比使用 WebView 跨平臺方式,優勢確實很是明顯。基於 WebView 的框架集成了當下 Web 開發的諸多優點:豐富的控件庫、動態化、良好的技術社區、測試自動化等等。可是缺點也一樣明顯:渲染效率和 JavaScript 的執行能力都比較差,使頁面的加載速度和用戶體驗都不盡如人意。ios
而使用以 React Native(簡稱 RN)爲表明的框架時,維護又成了大難題。RN 使用類 HTML+JS 的 UI 建立邏輯,生成對應的原生頁面,將頁面的渲染工做交給了系統,因此渲染效率有很大的優點。但因爲 RN 代碼是經過 JS 橋接的方式轉換爲原生的控件,因此受各個系統間的差別影響很是大,雖然能夠開發一套代碼,但對各個平臺的適配卻很是的繁瑣和麻煩。git
2018 年 12 月初,Google 正式發佈了開源跨平臺 UI 框架 Flutter 1.0 Release 版本,馬蜂窩電商客戶端團隊進行了調研與實踐,發現 Flutter 能很好的幫助咱們解決開發中遇到的問題。github
因而,電商客戶端團隊決定探索 Flutter 在跨平臺開發中的新可能,並率先應用於商家端 App 中。在本文中,咱們將結合 Flutter 在馬蜂窩商家端 App 中的應用實踐,探討 Flutter 架構的實現原理,有何優點,以及如何幫助咱們解決問題。web
Flutter 使用 Dart 語言開發,主要有如下幾點緣由:xcode
Dart 主要由 Google 負責開發和維護。目前 Dart 最新版本已是 2.2,針對 App 和 Web 開發作了不少優化。而且對於大多數的開發者而言,Dart 的學習成本很是低。前端工程師
Flutter 架構也是採用的分層設計。從下到上依次爲:Embedder(嵌入器)、Engine、Framework。多線程
<center>圖 1: Flutter 分層架構圖</center>架構
Embedder是嵌入層,作好這一層的適配 Flutter 基本能夠嵌入到任何平臺上去; Engine層主要包含 Skia、Dart 和 Text。Skia 是開源的二位圖形庫;Dart 部分主要包括 runtime、Garbage Collection、編譯模式支持等;Text 是文本渲染。Framework在最上層。咱們的應用圍繞 Framework 層來構建,所以也是本文要介紹的重點。
1.【Foundation】在最底層,主要定義底層工具類和方法,以提供給其餘層使用。
2.【Animation】是動畫相關的類,能夠基於此建立補間動畫(Tween Animation)和物理原理動畫(Physics-based Animation),相似 Android 的 ValueAnimator 和 iOS 的 Core Animation。
3.【Painting】封裝了 Flutter Engine 提供的繪製接口,例如繪製縮放圖像、插值生成陰影、繪製盒模型邊框等。
4.【Gesture】提供處理手勢識別和交互的功能。
5.【Rendering】是框架中的渲染庫。控件的渲染主要包括三個階段:佈局(Layout)、繪製(Paint)、合成(Composite)。
從下圖能夠看到,Flutter 流水線包括 7 個步驟。
<center>圖 2: Flutter 流水線</center>
首先是獲取到用戶的操做,而後你的應用會所以顯示一些動畫,接着 Flutter 開始構建 Widget 對象。
Widget 對象構建完成後進入渲染階段,這個階段主要包括三步:
最後的光柵化由 Engine 層來完成。
在渲染階段,控件樹(widget)會轉換成對應的渲染對象(RenderObject)樹,在 Rendering 層進行佈局和繪製。
在佈局時 Flutter 深度優先遍歷渲染對象樹。數據流的傳遞方式是從上到下傳遞約束,從下到上傳遞大小。也就是說,父節點會將本身的約束傳遞給子節點,子節點根據接收到的約束來計算本身的大小,而後將本身的尺寸返回給父節點。整個過程當中,位置信息由父節點來控制,子節點並不關心本身所在的位置,而父節點也不關心子節點具體長什麼樣子。
<center>圖 3: 數據流傳遞方式</center>
爲了防止因子節點發生變化而致使的整個控件樹重繪,Flutter 加入了一個機制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 會被自動建立,不須要開發者手動添加。
例如,控件被設置了固定大小(tight constraint)、控件忽略全部子視圖尺寸對本身的影響、控件自動佔滿父控件所提供的空間等等。很好理解,就是控件大小不會影響其餘控件時,就不必從新佈局整個控件樹。有了這個機制後,不管子樹發生什麼樣的變化,處理範圍都只在子樹上。
<center>圖 4: Relayout Boundary 機制</center>
在肯定每一個空間的位置和大小以後,就進入繪製階段。繪製節點的時候也是深度遍歷繪製節點樹,而後把不一樣的 RenderObject 繪製到不一樣的圖層上。
這時有可能出現一種特殊狀況,以下圖所示節點 2 在繪製子節點 4 時,因爲其節點 4 須要單獨繪製到一個圖層上(如 video),所以綠色圖層上面多了個黃色的圖層。以後再須要繪製其餘內容(標記 5)就須要再增長一個圖層(紅色)。再接下來要繪製節點 1 的右子樹(標記 6),也會被繪製到紅色圖層上。因此若是 2 號節點發生改變就會改變紅色圖層上的內容,所以也影響到了絕不相干的 6 號節點。
<center>圖 5: 繪製節點與圖層的關係</center>
爲了不這種狀況,Flutter 的設計者這裏基於 Relayout Boundary 的思想增長了Repaint Boundary。在繪製頁面時候若是碰見 Repaint Boundary 就會強制切換圖層。
以下圖所示,在從上到下遍歷控件樹遇到 Repaint Boundary 會從新繪製到新的圖層(深藍色),在從下到上返回的時候又遇到 Repaint Boundary,因而又增長一個新的圖層(淺藍色)。
<center>圖 6: Repaint Boundary 機制</center>
這樣,即便發生重繪也不會對其餘子樹產生影響。好比在 Scrollview 上,當滾動的時候發生內容重繪,若是在 Scrollview 之外的地方不須要重繪就可使用 Repaint Boundary。Repaint Boundary 並不會像 Relayout Boundary 同樣自動生成,而是須要咱們本身來加入到控件樹中。
6.【Widget】控件層。全部控件的基類都是 Widget,Widget 的數據都是隻讀的, 不能改變。因此每次須要更新頁面時都須要從新建立一個新的控件樹。每個 Widget 會經過一個 RenderObjectElement 對應到一個渲染節點(RenderObject),能夠簡單理解爲 Widget 中只存儲了頁面元素的信息,而真正負責佈局、渲染的是 RenderObject。
在頁面更新從新生成控件樹時,RenderObjectElement 樹會盡可能保持重用。因爲 RenderObjectElement 持有對應的 RenderObject,全部 RenderObject 樹也會盡量的被重用。如圖所示就是三棵樹之間的關係。在這張圖裏咱們把形狀當作渲染節點的類型,顏色是它的屬性,即形狀不一樣就是不一樣的渲染節點,而顏色不一樣只是同一對象的屬性的不一樣。
<center>圖 7:Widget、Element 和 Render 之間的關係</center>
若是想把方形的顏色換成黃色,將圓形的顏色變成紅色,因爲控件是不能被修改的,須要從新生成兩個新的控件 Rectangle yellow 和 Circle red。因爲只是修改了顏色屬性,因此 Element 和 RenderObject 都被重用,而以前的控件樹會被釋放回收。
<center>圖 8: 示例</center>
那麼若是把紅色圓形變成三角形又會怎樣呢?因爲這裏發生變化的是類型,因此對應的 Element 節點和 RenderObject 節點都須要從新建立。可是因爲黃色方形沒有發生改變,因此其對應的 Element 節點和 RenderObject 節點沒有發生變化。
<center>圖 9: 示例</center>
7. 最後是【Material】 & 【Cupertino】,這是在 Widget 層之上框架爲開發者提供的基於兩套設計語言實現的 UI 控件,能夠幫助咱們的 App 在不一樣平臺上提供接近原生的用戶體驗。
<center>圖 10: 馬蜂窩商家端使用 Flutter 開發的頁面</center>
因爲商家端已是一款成熟的 App,不可能建立一個新的 Flutter 工程所有從新開發,所以咱們選擇 Native 與 Flutter 混編的方案來實現。
在瞭解 Native 與 Flutter 混編方案前,首先咱們須要瞭解在 Flutter 工程中,一般有如下 4 種工程類型:
1. Flutter Application
標準的 Flutter App 工程,包含標準的 Dart 層與 Native 平臺層。
2. Flutter Module
Flutter 組件工程,僅包含 Dart 層實現,Native 平臺層子工程爲經過 Flutter 自動生成的隱藏工程(.ios /.android)。
3. Flutter Plugin
Flutter 平臺插件工程,包含 Dart 層與 Native 平臺層的實現。
4. Flutter Package
Flutter 純 Dart 插件工程,僅包含 Dart 層的實現,每每定義一些公共 Widget。
瞭解了 Flutter 工程類型後,咱們來看下官方提供的一種混編方案(https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps),即在現有工程下建立Flutter Module 工程,以本地依賴的方式集成到現有的 Native 工程中。
官方集成方案(以 iOS 爲例)
a. 在工程目錄建立 FlutterModule,建立後,工程目錄大體以下:
b. 在 Podfile 文件中添加如下代碼:
flutter_application_path = '../flutter_Moudule/'
該腳本主要負責:
c. 在 iOS 構建階段 Build Phases 中注入構建時須要執行的 xcode_backend.sh (位於 FlutterSDK/packages/flutter_tools/bin) 腳本:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
該腳本主要負責:
d. 與 Native 通訊
以上就是官方提供的集成方案。咱們最終沒有選擇此方案的緣由,是它直接依賴於 FlutterModule 工程以及 Flutter 環境,使 Native 開發同窗沒法脫離 Flutter 環境開發,影響正常的開發流程,團隊合做成本較大;並且會影響正常的打包流程。(目前 Flutter 團隊正在重構嵌入 Native 工程的方式)
最終咱們選擇另外一種方案來解決以上的問題:遠端依賴產物。
<center>圖 11 :遠端依賴產物</center>
經過對官方混編方案的研究,咱們瞭解到 iOS 工程最終依賴的實際上是 FlutterModule 工程構建出的產物(Framework,Asset,Plugin),只需將產物導出並 push 到遠端倉庫,iOS 工程經過遠端依賴產物便可。
依賴產物目錄結構以下:
Android Nativite 集成是經過 Gradle 遠程依賴 Flutter 工程產物的方式完成的,如下是具體的集成流程。
a.建立 Flutter 標準工程
$ flutter create flutter_demo
默認使用 Java 代碼,若是增長 Kotlin 支持,使用以下命令:
$ flutter create -a kotlin flutter_demo
b.修改工程的默認配置
在集成過程當中 Flutter 依賴了三方 Plugins 後,遇到 Plugins 的代碼沒有被打進 Library 中的問題。經過如下配置解決(這種方式略顯粗暴,後續的優化方案正在調研)。
subprojects { project.buildDir = "${rootProject.buildDir}/app" }
$ cd android $ ./gradlew uploadArchives
官方默認的構建腳本在 Flutter 1.0.0 版本存在 Bug——最終的產物中會缺乏 flutter_shared/icudtl.dat 文件,致使 App Crash。目前的解決方式是將這個文件複製到工程的 assets 下(在 Flutter 最新 1.2.1 版本中這個 Bug 已被修復,可是 1.2.1 版本又出現了一個 UI 渲染的問題,因此只能繼續使用 1.0.0 版本)。
d.Android Native 平臺工程集成,增長下面依賴配置便可,不會影響 Native 平臺開發的同窗
implementation 'com.mfw.app:MerchantFlutter:0.0.5-beta'
使用平臺通道(Platform Channels)在 Flutter 工程和宿主(Native 工程)之間傳遞消息,主要是經過 MethodChannel 進行方法的調用,以下圖所示:
<center>圖 12 :Flutter 與 iOS、Android 交互</center>
爲了確保用戶界面不會掛起,消息和響應是異步傳遞的,須要用 async 修飾方法,await 修飾調用語句。Flutter 工程和宿主工程經過在 Channel 構造函數中傳遞 Channel 名稱進行關聯。單個應用中使用的全部 Channel 名稱必須是惟一的; 能夠在 Channel 名稱前加一個惟一的「域名前綴」。
咱們分別使用 Native 和 Flutter 開發了兩個列表頁,如下是頁面效果和性能對比:
Flutter 頁面:
iOS Native 頁面:
能夠看到,從使用和直觀感覺都沒有太大的差異。因而咱們採集了一些其餘方面的數據。
Flutter 頁面:
iOS Native 頁面:
另外咱們還對比了商家端接入 Flutter 先後包體積的大小:39Mb → 44MB
在 iOS 機型上,流暢度上沒有什麼差別。從數值上來看,Flutter 在 內存跟 GPU/CPU 使用率上比原生略高。Demo 中並無對 Flutter 作更多的優化,能夠看出 Flutter 總體來講仍是能夠作出接近於原生的頁面。
下面是 Flutter 與 Android 的性能對比。
Flutter 頁面:
Android Native 頁面:
從以上兩張對比圖能夠看出,不考慮其餘因素,單純從性能角度來講,原生要優於 Flutter,可是差距並不大,並且 Flutter 具備的跨平臺開發和熱重載等特色極大地節省了開發效率。而且,將來的熱修復特性更是值得期待。
首先先介紹下 Flutter 路由的管理:
<center>圖 14 :Flutter 路由管理</center>
若是是純 Flutter 工程,頁面棧無需咱們進行管理,可是引入到 Native 工程內,就須要考慮如何管理混合棧。而且須要解決如下幾個問題:
1. 保證 Flutter 頁面與 Native 頁面之間的跳轉從用戶體驗上沒有任何差別
2. 頁面資源化(馬蜂窩特有的業務邏輯)
3. 保證生命週期完整性,處理相關打點事件上報
4. 資源性能問題
參考了業界內的解決方法,以及項目自身的實際場景,咱們選擇相似於 H5 在 Navite 中嵌入的方式,統一經過 openURL 跳轉到一個 Native 頁面(FlutterContainerVC),Native 頁面經過 addChildViewController 方式添加 FlutterViewController(負責 Flutter 頁面渲染),同時經過 channel 同步 Native 頁面與 Flutter 頁面。
Flutter 一經發布就很受關注,除了 iOS 和 Android 的開發者,不少前端工程師也都很是看好 Flutter 將來的發展前景。相信也有不少公司的團隊已經投入到研究和實踐中了。不過 Flutter 也有不少不足的地方,值得咱們注意:
目前阿里的閒魚開發團隊已經將 Flutter 用於大型實踐,並應用在了比較重要的場景(如產品詳情頁),爲後來者提供了良好的借鑑。馬蜂窩的移動客戶端團隊關於 Flutter 的探索纔剛剛起步,前面還有不少的問題須要咱們一點一點去解決。不過不管從 Google 對其的重視程度,仍是咱們從實踐中看到的這些優勢,都讓咱們對 Flutter 充滿信心,也但願在將來咱們能夠利用它創造更多的價值和奇蹟。
路途雖遠,猶可期許。
本文做者:馬蜂窩電商研發客戶端團隊。
(馬蜂窩技術原創內容,轉載務必註明出處保存文末二維碼圖片,禁止商業用途,謝謝配合。)
參考文獻:
關注馬蜂窩技術,找到更多你想要的內容