Flutter 實現原理及在馬蜂窩的跨平臺開發實踐

一直以來,跨平臺開發都是困擾移動客戶端開發的難題。前端

在馬蜂窩旅遊 App 不少業務場景裏,咱們嘗試過一些主流的跨平臺開發解決方案,好比 WebView 和 React Native,來提高開發效率和用戶體驗。但這兩種方式也帶來了新的問題。android

好比使用 WebView 跨平臺方式,優勢確實很是明顯。基於 WebView 的框架集成了當下 Web 開發的諸多優點:豐富的控件庫、動態化、良好的技術社區、測試自動化等等。可是缺點也一樣明顯:渲染效率和 JavaScript 的執行能力都比較差,使頁面的加載速度和用戶體驗都不盡如人意。ios

而使用以 React Native(簡稱 RN)爲表明的框架時,維護又成了大難題。RN 使用類 HTML+JS 的 UI 建立邏輯,生成對應的原生頁面,將頁面的渲染工做交給了系統,因此渲染效率有很大的優點。但因爲 RN 代碼是經過 JS 橋接的方式轉換爲原生的控件,因此受各個系統間的差別影響很是大,雖然能夠開發一套代碼,但對各個平臺的適配卻很是的繁瑣和麻煩。git

爲何是 Flutter

2018 年 12 月初,Google 正式發佈了開源跨平臺 UI 框架 Flutter 1.0 Release 版本,馬蜂窩電商客戶端團隊進行了調研與實踐,發現 Flutter 能很好的幫助咱們解決開發中遇到的問題。github

  1. 跨平臺開發針對 Android 與 iOS 的風格設計了兩套設計語言的控件實現(Material & Cupertino)。這樣不但可以節約人力成本,並且在用戶體驗上更好的適配 App 運行的平臺。web

  2. 重寫了一套跨平臺的 UI 框架,渲染引擎是依靠 Skia 圖形庫實現。Flutter 中的控件樹直接由渲染引擎和高性能本地 ARM 代碼直接繪製,不須要經過中間對象(Web 應用中的虛擬 DOM 和真實 DOM,原生 App 中的虛擬控件和平臺控件)來繪製,使它有接近原生頁面的性能,幫助咱們提供更好的用戶體驗。xcode

  3. 同時支持 JIT 和 AOT 編譯。JIT 編譯方式使其在開發階段有個備受歡迎的功能——熱重載(HotReload),這樣在開發時能夠省去構建的過程,提升開發效率。而在 Release 運行階段採用 AOT 的編譯方式,使執行效率很是高,讓 Release 版本發揮更好的性能。bash

因而,電商客戶端團隊決定探索 Flutter 在跨平臺開發中的新可能,並率先應用於商家端 App 中。在本文中,咱們將結合 Flutter 在馬蜂窩商家端 App 中的應用實踐,探討 Flutter 架構的實現原理,有何優點,以及如何幫助咱們解決問題。前端工程師

Flutter 架構和實現原理

Flutter 使用 Dart 語言開發,主要有如下幾點緣由:多線程

  • Dart 通常狀況下是運行 DartVM 上,可是也能夠編譯爲 ARM 代碼直接運行在硬件上。

  • Dart 同時支持 AOT 和 JIT 兩種編譯方式,能夠更好的提升開發以及 App 的執行效率。

  • Dart 能夠利用獨特的隔離區(Isolate)實現多線程。並且不共享內存,能夠實現無鎖快速分配。

  • 分代垃圾回收,很是適合 UI 框架中常見的大量 Widgets 對象建立和銷燬的優化。

  • 在爲建立的對象分配內存時,Dart 是在現有的堆上移動指針,保證內存的增加是程線性的,因而就省了查找可用內存的過程。

Dart 主要由 Google 負責開發和維護。目前 Dart 最新版本已是 2.2,針對 App 和 Web 開發作了不少優化。而且對於大多數的開發者而言,Dart 的學習成本很是低。

Flutter 架構也是採用的分層設計。從下到上依次爲:Embedder(嵌入器)、Engine、Framework。

圖 1: Flutter 分層架構圖

Embedder 是嵌入層,作好這一層的適配 Flutter 基本能夠嵌入到任何平臺上去; Engine 層主要包含 Skia、Dart 和 Text。Skia 是開源的二位圖形庫;Dart 部分主要包括 runtime、Garbage Collection、編譯模式支持等;Text 是文本渲染。Framework 在最上層。咱們的應用圍繞 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 個步驟。

圖 2: Flutter 流水線

首先是獲取到用戶的操做,而後你的應用會所以顯示一些動畫,接着 Flutter 開始構建 Widget 對象。

Widget 對象構建完成後進入渲染階段,這個階段主要包括三步:

  • 佈局元素:決定頁面元素在屏幕上的位置和大小;

  • 繪製階段:將頁面元素繪製成它們應有的樣式;

  • 合成階段:按照繪製規則將以前兩個步驟的產物組合在一塊兒。

最後的光柵化由 Engine 層來完成。

在渲染階段,控件樹(widget)會轉換成對應的渲染對象(RenderObject)樹,在 Rendering 層進行佈局和繪製。

在佈局時 Flutter 深度優先遍歷渲染對象樹。數據流的傳遞方式是從上到下傳遞約束,從下到上傳遞大小。也就是說,父節點會將本身的約束傳遞給子節點,子節點根據接收到的約束來計算本身的大小,而後將本身的尺寸返回給父節點。整個過程當中,位置信息由父節點來控制,子節點並不關心本身所在的位置,而父節點也不關心子節點具體長什麼樣子。

圖 3: 數據流傳遞方式

爲了防止因子節點發生變化而致使的整個控件樹重繪,Flutter 加入了一個機制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 會被自動建立,不須要開發者手動添加。

例如,控件被設置了固定大小(tight constraint)、控件忽略全部子視圖尺寸對本身的影響、控件自動佔滿父控件所提供的空間等等。很好理解,就是控件大小不會影響其餘控件時,就不必從新佈局整個控件樹。有了這個機制後,不管子樹發生什麼樣的變化,處理範圍都只在子樹上。

圖 4: Relayout Boundary 機制

在肯定每一個空間的位置和大小以後,就進入繪製階段。繪製節點的時候也是深度遍歷繪製節點樹,而後把不一樣的 RenderObject 繪製到不一樣的圖層上。

這時有可能出現一種特殊狀況,以下圖所示節點 2 在繪製子節點 4 時,因爲其節點 4 須要單獨繪製到一個圖層上(如 video),所以綠色圖層上面多了個黃色的圖層。以後再須要繪製其餘內容(標記 5)就須要再增長一個圖層(紅色)。再接下來要繪製節點 1 的右子樹(標記 6),也會被繪製到紅色圖層上。因此若是 2 號節點發生改變就會改變紅色圖層上的內容,所以也影響到了絕不相干的 6 號節點。

圖 5: 繪製節點與圖層的關係

爲了不這種狀況,Flutter 的設計者這裏基於 Relayout Boundary 的思想增長了 Repaint Boundary。在繪製頁面時候若是碰見 Repaint Boundary 就會強制切換圖層。

以下圖所示,在從上到下遍歷控件樹遇到 Repaint Boundary 會從新繪製到新的圖層(深藍色),在從下到上返回的時候又遇到 Repaint Boundary,因而又增長一個新的圖層(淺藍色)。

圖 6: Repaint Boundary 機制

這樣,即便發生重繪也不會對其餘子樹產生影響。好比在 Scrollview 上,當滾動的時候發生內容重繪,若是在 Scrollview 之外的地方不須要重繪就可使用 Repaint Boundary。Repaint Boundary 並不會像 Relayout Boundary 同樣自動生成,而是須要咱們本身來加入到控件樹中。

6.【Widget】控件層。全部控件的基類都是 Widget,Widget 的數據都是隻讀的, 不能改變。因此每次須要更新頁面時都須要從新建立一個新的控件樹。每個 Widget 會經過一個 RenderObjectElement 對應到一個渲染節點(RenderObject),能夠簡單理解爲 Widget 中只存儲了頁面元素的信息,而真正負責佈局、渲染的是 RenderObject。

在頁面更新從新生成控件樹時,RenderObjectElement 樹會盡可能保持重用。因爲 RenderObjectElement 持有對應的 RenderObject,全部 RenderObject 樹也會盡量的被重用。如圖所示就是三棵樹之間的關係。在這張圖裏咱們把形狀當作渲染節點的類型,顏色是它的屬性,即形狀不一樣就是不一樣的渲染節點,而顏色不一樣只是同一對象的屬性的不一樣。

圖 7: Widget、Element 和 Render 之間的關係

若是想把方形的顏色換成黃色,將圓形的顏色變成紅色,因爲控件是不能被修改的,須要從新生成兩個新的控件 Rectangle yellow 和 Circle red。因爲只是修改了顏色屬性,因此 Element 和 RenderObject 都被重用,而以前的控件樹會被釋放回收。

圖 8: 示例

那麼若是把紅色圓形變成三角形又會怎樣呢?因爲這裏發生變化的是類型,因此對應的 Element 節點和 RenderObject 節點都須要從新建立。可是因爲黃色方形沒有發生改變,因此其對應的 Element 節點和 RenderObject 節點沒有發生變化。

圖 9: 示例

7. 最後是【Material】 & 【Cupertino】,這是在 Widget 層之上框架爲開發者提供的基於兩套設計語言實現的 UI 控件,能夠幫助咱們的 App 在不一樣平臺上提供接近原生的用戶體驗。

Flutter 在馬蜂窩商家端App 中的應用實踐

圖 10: 馬蜂窩商家端使用 Flutter 開發的頁面

開發方式:Flutter + Native

因爲商家端已是一款成熟的 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 工程類型後,咱們來看下官方提供的一種混編方案(github.com/flutter/flu…),即在現有工程下建立 Flutter Module 工程,以本地依賴的方式集成到現有的 Native 工程中。

官方集成方案(以 iOS 爲例)

a. 在工程目錄建立 FlutterModule,建立後,工程目錄大體以下:

b. 在 Podfile 文件中添加如下代碼:

flutter_application_path = '../flutter_Moudule/'
複製代碼

該腳本主要負責:

  • pod 引入 Flutter.Framework 以及 FlutterPluginRegistrant 註冊入口

  • pod 引入 Flutter 第三方 plugin

  • 在每個 pod 庫的配置文件中寫入對 Generated.xcconfig 文件的導入

  • 修改 pod 庫的 ENABLE_BITCODE = NO(由於 Flutter 如今不支持 bitcode)

c. 在 iOS 構建階段 Build Phases 中注入構建時須要執行的 xcode_backend.sh (位於 FlutterSDK/packages/flutter_tools/bin) 腳本:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

複製代碼

該腳本主要負責:

  • 構建 App.framework 以及 Flutter.framework 產物

  • 根據編譯模式(debug/profile/release)導入對應的產物

  • 編譯 flutter_asset 資源

  • 把以上產物 copy 到對應的構建產物中

d. 與 Native 通訊

  • 方案一:改造 AppDelegate 繼承自 FlutterAppDelegate

  • 方案二:AppDelegate 實現 FlutterAppLifeCycleProvider 協議,生命週期由 FlutterPluginAppLifeCycleDelegate 傳遞給 Flutter

以上就是官方提供的集成方案。咱們最終沒有選擇此方案的緣由,是它直接依賴於 FlutterModule 工程以及 Flutter 環境,使 Native 開發同窗沒法脫離 Flutter 環境開發,影響正常的開發流程,團隊合做成本較大;並且會影響正常的打包流程。(目前 Flutter 團隊正在重構嵌入 Native 工程的方式)

最終咱們選擇另外一種方案來解決以上的問題:遠端依賴產物

圖 11 :遠端依賴產物

iOS 集成方案

經過對官方混編方案的研究,咱們瞭解到 iOS 工程最終依賴的實際上是 FlutterModule 工程構建出的產物(Framework,Asset,Plugin),只需將產物導出並 push 到遠端倉庫,iOS 工程經過遠端依賴產物便可。

依賴產物目錄結構以下:

  • App.framework: Flutter 工程產物(包含 Flutter 工程的代碼,Debug 模式下它是個空殼,代碼在 flutter_assets 中)。

  • **Flutter.framework:*Flutter 引擎庫。與編譯模式(debug/profile/release)以及 CPU 架構(arm, i386, x86_64)相匹配。

  • lib.a & .h 頭文件: FlutterPlugin 靜態庫(包含在 iOS 端的實現)。

  • flutter_assets: 包含 Flutter 工程字體,圖片等資源。在 Flutter1.2 版本中,被打包到 App.framework 中。

Android 集成方案

Android Nativite 集成是經過 Gradle 遠程依賴 Flutter 工程產物的方式完成的,如下是具體的集成流程。

a.建立 Flutter 標準工程

$ flutter create flutter_demo

複製代碼

默認使用 Java 代碼,若是增長 Kotlin 支持,使用以下命令:

$ flutter create -a kotlin flutter_demo

複製代碼

b. 修改工程的默認配置

  1. 修改 app module 工程的 build.gradle 配置  apply plugin: 'com.android.application' => apply plugin: 'com.android.library',並移除 applicationId 配置

  2. 修改 root 工程的 build.gradle 配置

    在集成過程當中 Flutter 依賴了三方 Plugins 後,遇到 Plugins 的代碼沒有被打進 Library 中的問題。經過如下配置解決(這種方式略顯粗暴,後續的優化方案正在調研)。

subprojects {
   project.buildDir = "${rootProject.buildDir}/app"
}
複製代碼
  1. app module 增長 maven 打包配置
  2. c. 生成 Android Flutter 產物
$ 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'
複製代碼

Flutter 和 iOS、Android 的交互

使用平臺通道(Platform Channels)在 Flutter 工程和宿主(Native 工程)之間傳遞消息,主要是經過 MethodChannel 進行方法的調用,以下圖所示:

圖 12 :Flutter 與 iOS、Android 交互

爲了確保用戶界面不會掛起,消息和響應是異步傳遞的,須要用 async 修飾方法,await 修飾調用語句。Flutter 工程和宿主工程經過在 Channel 構造函數中傳遞 Channel 名稱進行關聯。單個應用中使用的全部 Channel 名稱必須是惟一的; 能夠在 Channel 名稱前加一個惟一的「域名前綴」。

Flutter 與 Native 性能對比

咱們分別使用 Native 和 Flutter 開發了兩個列表頁,如下是頁面效果和性能對比:

iOS 對比(機型 6P 系統 10.3.3):

Flutter 頁面:

iOS Native 頁面:

能夠看到,從使用和直觀感覺都沒有太大的差異。因而咱們採集了一些其餘方面的數據。

Flutter 頁面:

iOS Native 頁面:

另外咱們還對比了商家端接入 Flutter 先後包體積的大小:39Mb→44MB

在 iOS 機型上,流暢度上沒有什麼差別。從數值上來看,Flutter 在 內存跟 GPU/CPU 使用率上比原生略高。Demo 中並無對 Flutter 作更多的優化,能夠看出 Flutter 總體來講仍是能夠作出接近於原生的頁面。

下面是 Flutter 與 Android 的性能對比

Flutter 頁面:

Android Native 頁面:

從以上兩張對比圖能夠看出,不考慮其餘因素,單純從性能角度來講,原生要優於 Flutter,可是差距並不大,並且 Flutter 具備的跨平臺開發和熱重載等特色極大地節省了開發效率。而且,將來的熱修復特性更是值得期待。

混合棧管理

首先先介紹下 Flutter 路由的管理:

  • Flutter 管理頁面有兩個概念:Route 和 Navigator。

  • Navigator 是一個路由管理的 Widget(Flutter 中萬物皆 Widget),它經過一個棧來管理一個路由 Widget 集合。一般當前屏幕顯示的頁面就是棧頂的路由。

  • 路由 (Route) 在移動開發中一般指頁面(Page),這跟 web 開發中單頁應用的 Route 概念意義是相同的,Route 在 Android 中一般指一個 Activity,在 iOS 中指一個 ViewController。所謂路由管理,就是管理頁面之間如何跳轉,一般也可被稱爲導航管理。這和原生開發相似,不管是 Android 仍是 iOS,導航管理都會維護一個路由棧,路由入棧 (push) 操做對應打開一個新頁面,路由出棧 (pop) 操做對應頁面關閉操做,而路由管理主要是指如何來管理路由棧。

圖 14 :Flutter 路由管理

若是是純 Flutter 工程,頁面棧無需咱們進行管理,可是引入到 Native 工程內,就須要考慮如何管理混合棧。而且須要解決如下幾個問題:

1. 保證 Flutter 頁面與 Native 頁面之間的跳轉從用戶體驗上沒有任何差別

2. 頁面資源化(馬蜂窩特有的業務邏輯)

3. 保證生命週期完整性,處理相關打點事件上報

4. 資源性能問題

參考了業界內的解決方法,以及項目自身的實際場景,咱們選擇相似於 H5 在 Navite 中嵌入的方式,統一經過 openURL 跳轉到一個 Native 頁面(FlutterContainerVC),Native 頁面經過 addChildViewController 方式添加 FlutterViewController(負責 Flutter 頁面渲染),同時經過 channel 同步 Native 頁面與 Flutter 頁面。

  • 每一次的 push/pop 由 Native 發起,同時經過 channel 保持 Native 與 Flutter 頁面同步——在 Native 中跳轉 Flutter 頁面與跳轉原生無差別

  • 一個 Flutter 頁面對應一個 Native 頁面(FlutterContainerVC)——解決頁面資源化

  • FlutterContainerVC 經過 addChildViewController 對單例 FlutterViewController 進行復用——保證生命週期完整性,處理相關打點事件上報

  • 因爲每個 FlutterViewController(提供 Flutter 視圖的實現)會啓動三個線程,分別是 UI 線程、GPU 線程和 IO 線程,使用單例 FlutterViewController 能夠減小對資源的佔用——解決資源性能問題

Flutter 應用總結

Flutter 一經發布就很受關注,除了 iOS 和 Android 的開發者,不少前端工程師也都很是看好 Flutter 將來的發展前景。相信也有不少公司的團隊已經投入到研究和實踐中了。不過 Flutter 也有不少不足的地方,值得咱們注意:

  1. 雖然 1.2 版本已經發布,可是目前沒有達到徹底穩定狀態,1.2 發佈完了就出現了控件渲染的問題。加上 Dart 語言生態小,學習資料可能不夠豐富。

  2. 關於動態化的支持,目前 Flutter 還不支持線上動態性。若是要在 Android 上實現動態性相對容易些,iOS 因爲審覈緣由要實現動態性可能成本很高。

  3. Flutter 中目前拿來就用的能力只有 UI 控件和 Dart 自己提供能力,對於平臺級別的能力還須要經過 channel 的方式來擴展。

  4. 已有工程遷移比較複雜,之前沉澱的 UI 控件,須要從新再實現一套。

  5. 最後一點比較有爭議,Flutter 不會從程序中拆分出額外的模板或佈局語言,如 JSX 或 XM L,也不須要單獨的可視佈局工具。有的人認爲配合 HotReload 功能使用很是方便,但咱們發現這樣代碼會有很是多的嵌套,閱讀起來有些吃力。

目前阿里的閒魚開發團隊已經將 Flutter 用於大型實踐,並應用在了比較重要的場景(如產品詳情頁),爲後來者提供了良好的借鑑。馬蜂窩的移動客戶端團隊關於 Flutter 的探索纔剛剛起步,前面還有不少的問題須要咱們一點一點去解決。不過不管從 Google 對其的重視程度,仍是咱們從實踐中看到的這些優勢,都讓咱們對 Flutter 充滿信心,也但願在將來咱們能夠利用它創造更多的價值和奇蹟。

路途雖遠,猶可期許。

本文做者:馬蜂窩電商研發客戶端團隊。

(馬蜂窩技術原創內容,轉載務必註明出處保存文末二維碼圖片,謝謝配合。)

參考文獻:

關注馬蜂窩技術,找到更多你想要的內容

相關文章
相關標籤/搜索