天天看點

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

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

在馬蜂窩旅遊 App 很多業務場景裡,我們嘗試過一些主流的跨平台開發解決方案, 比如 WebView 和 React Native,來提升開發效率和使用者體驗。但這兩種方式也帶來了新的問題。

比如使用 WebView 跨平台方式,優點确實非常明顯。基于 WebView 的架構內建了當下 Web 開發的諸多優勢:豐富的控件庫、動态化、良好的技術社群、測試自動化等等。但是缺點也同樣明顯:渲染效率和 JavaScript 的執行能力都比較差,使頁面的加載速度和使用者體驗都不盡如人意。

而使用以 React Native(簡稱 RN)為代表的架構時,維護又成了大難題。RN 使用類 HTML+JS 的 UI 建立邏輯,生成對應的原生頁面,将頁面的渲染工作交給了系統,是以渲染效率有很大的優勢。但由于 RN 代碼是通過 JS 橋接的方式轉換為原生的控件,是以受各個系統間的差異影響非常大,雖然可以開發一套代碼,但對各個平台的适配卻非常的繁瑣和麻煩。

為什麼是 Flutter

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

  1. 跨平台開發,針對 Android 與 iOS 的風格設計了兩套設計語言的控件實作(Material & Cupertino)。這樣不但能夠節約人力成本,而且在使用者體驗上更好的适配 App 運作的平台。
  2. 重寫了一套跨平台的 UI 架構,渲染引擎是依靠 Skia 圖形庫實作。Flutter 中的控件樹直接由渲染引擎和高性能本地 ARM 代碼直接繪制,不需要通過中間對象(Web 應用中的虛拟 DOM 和真實 DOM,原生 App 中的虛拟控件和平台控件)來繪制,使它有接近原生頁面的性能,幫助我們提供更好的使用者體驗。
  3. 同時支援 JIT 和 AOT 編譯。JIT 編譯方式使其在開發階段有個備受歡迎的功能——熱重載(HotReload),這樣在開發時可以省去建構的過程,提高開發效率。而在 Release 運作階段采用 AOT 的編譯方式,使執行效率非常高,讓 Release 版本發揮更好的性能。

于是,電商用戶端團隊決定探索 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。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 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 個步驟。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 2: Flutter 流水線

首先是擷取到使用者的操作,然後你的應用會是以顯示一些動畫,接着 Flutter 開始建構 Widget 對象。

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

  • 布局元素:決定頁面元素在螢幕上的位置和大小;
  • 繪制階段:将頁面元素繪制成它們應有的樣式;
  • 合成階段:按照繪制規則将之前兩個步驟的産物組合在一起。

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

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

在布局時 Flutter 深度優先周遊渲染對象樹。資料流的傳遞方式是從上到下傳遞限制,從下到上傳遞大小。也就是說,父節點會将自己的限制傳遞給子節點,子節點根據接收到的限制來計算自己的大小,然後将自己的尺寸傳回給父節點。整個過程中,位置資訊由父節點來控制,子節點并不關心自己所在的位置,而父節點也不關心子節點具體長什麼樣子。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 3: 資料流傳遞方式

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

例如,控件被設定了固定大小(tight constraint)、控件忽略所有子視圖尺寸對自己的影響、控件自動占滿父控件所提供的空間等等。很好了解,就是控件大小不會影響其他控件時,就沒必要重新布局整個控件樹。有了這個機制後,無論子樹發生什麼樣的變化,處理範圍都隻在子樹上。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 4: Relayout Boundary 機制

在确定每個空間的位置和大小之後,就進入繪制階段。繪制節點的時候也是深度周遊繪制節點樹,然後把不同的 RenderObject 繪制到不同的圖層上。

這時有可能出現一種特殊情況,如下圖所示節點 2 在繪制子節點 4 時,由于其節點 4 需要單獨繪制到一個圖層上(如 video),是以綠色圖層上面多了個黃色的圖層。之後再需要繪制其他内容(标記 5)就需要再增加一個圖層(紅色)。再接下來要繪制節點 1 的右子樹(标記 6),也會被繪制到紅色圖層上。是以如果 2 号節點發生改變就會改變紅色圖層上的内容,是以也影響到了毫不相幹的 6 号節點。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 5: 繪制節點與圖層的關系

為了避免這種情況,Flutter 的設計者這裡基于 Relayout Boundary 的思想增加了 Repaint Boundary。在繪制頁面時候如果遇見 Repaint Boundary 就會強制切換圖層。

如下圖所示,在從上到下周遊控件樹遇到 Repaint Boundary 會重新繪制到新的圖層(深藍色),在從下到上傳回的時候又遇到 Repaint Boundary,于是又增加一個新的圖層(淺藍色)。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 6: Repaint Boundary 機制

這樣,即使發生重繪也不會對其他子樹産生影響。比如在 Scrollview 上,當滾動的時候發生内容重繪,如果在 Scrollview 以外的地方不需要重繪就可以使用 Repaint Boundary。Repaint Boundary 并不會像 Relayout Boundary 一樣自動生成,而是需要我們自己來加入到控件樹中。

6.【Widget】控件層。所有控件的基類都是 Widget,Widget 的資料都是隻讀的, 不能改變。是以每次需要更新頁面時都需要重新建立一個新的控件樹。每一個 Widget 會通過一個 RenderObjectElement 對應到一個渲染節點(RenderObject),可以簡單了解為 Widget 中隻存儲了頁面元素的資訊,而真正負責布局、渲染的是 RenderObject。

在頁面更新重新生成控件樹時,RenderObjectElement 樹會盡量保持重用。由于 RenderObjectElement 持有對應的 RenderObject,所有 RenderObject 樹也會盡可能的被重用。如圖所示就是三棵樹之間的關系。在這張圖裡我們把形狀當做渲染節點的類型,顔色是它的屬性,即形狀不同就是不同的渲染節點,而顔色不同隻是同一對象的屬性的不同。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

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

如果想把方形的顔色換成黃色,将圓形的顔色變成紅色,由于控件是不能被修改的,需要重新生成兩個新的控件 Rectangle yellow 和 Circle red。由于隻是修改了顔色屬性,是以 Element 和 RenderObject 都被重用,而之前的控件樹會被釋放回收。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 8: 示例

那麼如果把紅色圓形變成三角形又會怎樣呢?由于這裡發生變化的是類型,是以對應的 Element 節點和 RenderObject 節點都需要重新建立。但是由于黃色方形沒有發生改變,是以其對應的 Element 節點和 RenderObject 節點沒有發生變化。

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 9: 示例

7. 最後是【Material】 & 【Cupertino】,這是在 Widget 層之上架構為開發者提供的基于兩套設計語言實作的 UI 控件,可以幫助我們的 App 在不同平台上提供接近原生的使用者體驗。

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

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 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 工程類型後,我們來看下官方提供的一種混編方案(https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps ),即在現有工程下建立 Flutter Module 工程,以本地依賴的方式內建到現有的 Native 工程中。

官方內建方案(以 iOS 為例)

a. 在工程目錄建立 FlutterModule,建立後,工程目錄大緻如下:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

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 工程的方式)

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

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 11 :遠端依賴産物

iOS 內建方案

通過對官方混編方案的研究,我們了解到 iOS 工程最終依賴的其實是 FlutterModule 工程建構出的産物(Framework,Asset,Plugin),隻需将産物導出并 push 到遠端倉庫,iOS 工程通過遠端依賴産物即可。

依賴産物目錄結構如下:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
  • 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. 修改工程的預設配置

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
  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 進行方法的調用,如下圖所示:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 12 :Flutter 與 iOS、Android 互動

為了確定使用者界面不會挂起,消息和響應是異步傳遞的,需要用 async 修飾方法,await 修飾調用語句。Flutter 工程和宿主工程通過在 Channel 構造函數中傳遞 Channel 名稱進行關聯。單個應用中使用的所有 Channel 名稱必須是唯一的; 可以在 Channel 名稱前加一個唯一的「域名字首」。

Flutter 與 Native 性能對比

我們分别使用 Native 和 Flutter 開發了兩個清單頁,以下是頁面效果和性能對比:

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

Flutter 頁面:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
Flutter 實作原理及在馬蜂窩的跨平台開發實踐

iOS Native 頁面:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
Flutter 實作原理及在馬蜂窩的跨平台開發實踐

可以看到,從使用和直覺感受都沒有太大的差别。于是我們采集了一些其他方面的資料。

Flutter 頁面:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
Flutter 實作原理及在馬蜂窩的跨平台開發實踐

iOS Native 頁面:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐
Flutter 實作原理及在馬蜂窩的跨平台開發實踐
Flutter 實作原理及在馬蜂窩的跨平台開發實踐

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

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

在 iOS 機型上,流暢度上沒有什麼差異。從數值上來看,Flutter 在 記憶體跟 GPU/CPU 使用率上比原生略高。Demo 中并沒有對 Flutter 做更多的優化,可以看出 Flutter 整體來說還是可以做出接近于原生的頁面。

下面是 Flutter 與 Android 的性能對比。

Flutter 頁面:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

Android Native 頁面:

Flutter 實作原理及在馬蜂窩的跨平台開發實踐

從以上兩張對比圖可以看出,不考慮其他因素,單純從性能角度來說,原生要優于 Flutter,但是差距并不大,而且 Flutter 具有的跨平台開發和熱重載等特點極大地節省了開發效率。并且,未來的熱修複特性更是值得期待。

混合棧管理

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

  • Flutter 管理頁面有兩個概念:Route 和 Navigator。
  • Navigator 是一個路由管理的 Widget(Flutter 中萬物皆 Widget),它通過一個棧來管理一個路由 Widget 集合。通常目前螢幕顯示的頁面就是棧頂的路由。
  • 路由 (Route) 在移動開發中通常指頁面(Page),這跟 web 開發中單頁應用的 Route 概念意義是相同的,Route 在 Android 中通常指一個 Activity,在 iOS 中指一個 ViewController。所謂路由管理,就是管理頁面之間如何跳轉,通常也可被稱為導航管理。這和原生開發類似,無論是 Android 還是 iOS,導航管理都會維護一個路由棧,路由入棧 (push) 操作對應打開一個新頁面,路由出棧 (pop) 操作對應頁面關閉操作,而路由管理主要是指如何來管理路由棧。
Flutter 實作原理及在馬蜂窩的跨平台開發實踐

圖 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 充滿信心,也希望在未來我們可以利用它創造更多的價值和奇迹。

路途雖遠,猶可期許。

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

(馬蜂窩技術原創内容,轉載務必注明出處儲存文末二維碼圖檔,謝謝配合。)

參考文獻:

  • Flutter's Layered Design

    https://www.youtube.com/watch?v=dkyY9WCGMi0

  • Flutter's Rendering Pipeline

    https://www.youtube.com/watch?v=UUfXWzp0-DU&t=1955s

  • Flutter 原理與美團的實踐

    https://juejin.im/post/5b6d59476fb9a04fe91aa778#comment