京東技術中臺的Flutter實踐之路

在 2019 年,Flutter 推出了多個正式版本,支持的終端越來越多,使用的項目也越來越多。Flutter 正在經歷從小范圍嘗鮮到大面積應用的過程,越來越多的研發團隊加入到 Flutter 的學習熱潮中,京東作為互聯網大廠之一也積極參與了 Flutter 的跨端方案研究。本文將介紹京東在 Flutter 上的應用方案和相關優化成果。

為什么考慮Flutter技術方案

其實京東很早就開始研究并實踐跨端的開發解決方案,最早使用的是Hybrid App的技術方案,從2015年低開始逐步轉向RN技術棧,目前應該是業內RN技術平臺應用最廣泛、配套設施比較完善的公司之一。從2018年中開始,我們也關注到了Flutter技術,最吸引我們的特性是高性能和兼容性。這兩點也是目前RN技術相對不足的地方。高性能指的是復雜場景和交互下的渲染性能,兼容性指的是不同終端平臺上的布局和體驗的一致性,這點在碎片化嚴重的android平臺上尤其重要。

京東在Flutter的實踐

隨著2018年底Google正式發布了Flutter預覽版本,京東內部也越來越多的研發團隊有用Flutter進行開發業務的訴求。我們正式啟動研發并內部發布了JDFlutter引擎。在官方Flutter引擎之上,我們做了額外的優化和功能擴展:

  • Flutter工程改造: 對Flutter開發環境和dart代碼管理進行優化,可以無縫集成到現有APP中并支持自動化dart編譯打包,便于開發和調試。
  • 路由及多頁面管理: 對原生頁面和flutter頁面實現了集中路由管理,可以雙向傳參、跳轉并且進行了共享內存優化。
  • 擴展UI組件庫: 官方支持的Material和Cupertino樣式不能滿足需求,我們內部實現了自定義樣式的組件庫。
  • 原生能力擴展: 對官方原生能力進行了擴展,封裝了包括網絡、登陸、埋點等等基礎能力的打通并提供了50+原生擴展API。
  • Android端動態化支持: 在Android端實現了動態化支持,可以線上熱更新業務。iOS端暫不支持動態化。

目前京東商城、京東視頻、京東到家、京東物流、7Fresh等APP都有業務采用JDFlutter進行開發。

JDFlutter框架設計

JDFlutter整體的框架結構,主要包含:基礎框架、組件、工具三部分,如圖所示:

基礎框架

JDFlutter基礎框架分為三層架構,包含JDFlutter基礎層,通用業務層,業務層。

  • 基礎層:提供了Flutter的基礎組件支持,包括組件管理,狀態管理等;基礎層完全獨立,對業務沒有依賴。
  • 通用業務層:提供了通用型業務組件支持,例如登錄組件,支付組件等;通用業務層依賴于基礎層。
  • 業務層:即具體業務邏輯實現層,根據業務需要進行不同組件的組合,實現業務頁面的快速開發。
核心組件
  • 組件管理:組件之間通過標準的協議接口進行通信,降低組件耦合,便于維護及組件升級;
  • 狀態管理:實現數據和界面分離,統一狀態管理,以數據的變化來驅動界面的改變,更有利于數據的持久化和保存,同時也有利于UI組件的復用;
  • Hybrid Router:主要解決Flutter和Native之間交叉跳轉的問題,減少內存開銷,共享同一個Flutter Engine。
工具介紹
  • 編譯發布:優化Flutter原有的編譯邏輯,管理依賴Flutter原生依賴關聯,打包Flutter和原生代碼,實現自動化構建發布。
  • 資源管理:管理圖片資源,將資源轉換成Flutter類,便于資源的讀取操作,類似Andorid的R類;
  • 模版代碼生成:減少Flutter的代碼編寫,自動生成Flutter 組件的框架模板代碼,提升代碼編寫效率;
  • JSON轉換:將JSON數據轉換成Flutter code,并提供json轉Flutter對象的API,減少動手編寫Flutter code及解析。
JDFlutter業務開發實踐

JDFlutter為業務研發團隊提供了全流程的開發解決方案:

配置混合工程

Flutter和原生混合開發有兩種情況,其一,開發Flutter業務的同學,需要和原生做交互,因此需要有Flutter和原生的混合編譯環境;其二,使用原生SDK開發業務的同學,需要和Flutter業務一起集成打包,此時需對Flutter透明,以減少對Flutter編譯環境的依賴,并且,只依賴原生編譯環境即可,此時我們將Flutter編譯成aar依賴,放入原生項目中即可。接下來,我們將重點介紹Android和iOS的混合編譯環境配置。

Android平臺配置

創建一個flutter module

flutter create -t module --org com.example my_flutter

在原生根項目的settings.gradle加入如下配置信息

// MyApp/settings.gradle
include ':app'                        // assumed existing content
setBinding(new Binding([gradle: this]))              // new
evaluate(new File(                                   // new
settingsDir.parentFile,                              // new
  'my_flutter/.android/include_flutter.groovy'       // new
)) 

在原生App模塊中加入flutter依賴

dependencies {
  implementation project(':flutter')
}

這樣就可以原生項目一起編譯了。具體可以參照官方文檔:http://github.com/flutter/flu…這樣的方式雖可以滿足混編需求,但還不是特別方便,開發完項目后,還需要去Android Studio項目中進行編譯,比較麻煩,所以我們也可以把Flutter項目settings.gradle改造,在Flutter開發環境下直接運行包含原生代碼的混合項目,改造方式如下

// MyApp/settings.gradle
//projectName 原生模塊名稱
//projectPath 原生項目路徑
include ":$projectName"
project(":$projectName").projectDir = new File("$projectPath")

這樣改造之后即可在Flutter IDE中直接編譯Flutter混合工程,并進行調試,也可以運行futter run來啟動Flutter混合工程,不過在配置的時候,需要注意Flutter中 gradle編譯環境和原生編譯環境的一致性,如果不一致可能會導致編譯錯誤。

iOS平臺配置

創建flutter module

flutter create -t module my_flutter

進入iOS工程目錄,初始化pod環境(如果項目工程已經使用Cocoapods,跳過此步驟)

pod init

編輯Podfile文件

#在Podfile文件添加的新代碼
flutter_application_path = '/{flutter module目錄}/my_flutter'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

安裝pod

pod install

打開工程(***.xcworkspace) 配置build phase,為編譯Dart 代碼添加編譯選項打開iOS項目,選中項目的Build Phases選項,點擊左上角+號按鈕,選擇New Run Script Phase,將下面的shell腳本添加到輸入框中:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
搭建PUB私服倉庫

Flutter開發中使用的組件,一般公司內部會采用共享的方式,以避免重復開發,而Flutter組件共享,即需要使用pub倉庫。由于公司內部的業務組件不適合上傳到pub官方倉庫,因此,需要搭建私服倉庫,以解決各個業務研發團隊,對Flutter組件共享需要。感興趣的同學可以研究下官方pub倉庫的源碼 http://pub.dartlang.org/,其對Google Cloud 環境有很大的依賴 , 也可以基于https://github.com/kahnsen/pub_server來搭建一個簡易版本的私服倉庫,以滿足上傳和下載功能,pub協議相對比較簡單,我們可以在源碼增加協議接口來實現更多功能。運行pub_server

~ $ git clone https://github.com/dart-lang/pub_server.git
~ $ cd pub_server
~/pub_server $ pub get
...
~/pub_server $ dart example/example.dart -d /tmp/package-db
Listening on http://localhost:8080

To make the pub client use this repository configure your shell via:

    $ export PUB_HOSTED_URL=http://localhost:8080

發布一個Flutter組件需要修改 pubspec.yaml,增加以下內容:

name: hello_plugin //plugin名稱 
description: A new Flutter plugin. //介紹
version: 0.0.1//版本號
author: xxx <xxx@xxx.com>//作者和郵箱
homepage: https://localhost:8080 //組件的介紹頁面
publish_to: http://localhost:8080//倉庫上傳地址

上傳時可以使用如下命令檢查代碼錯誤,并顯示出上傳的目錄結構。

pub publish --dry-run

如果有不想上傳的文件,可以在根目錄增加一個.gitignore文件來忽略如下:

/build

Flutter組件的依賴配置,在項目的pubspec.yaml中dependencies:下增加如下信息:

dependencies:
hello_plugin:
  hosted:
    name: hello_plugin
    url: http://localhost:8080 
    version: 0.0.2

這樣可以在公司內部實現Flutter組件共享,如果不想搭建自己的pub倉庫,也可以采用git依賴,配置如下:

dependencies:
  hello_plugin:
    git:
      url: git://github.com/hello_plugin.git //git地址
      ref: dev-branch //分支
Flutter業務的開發與調試

在Flutter IDE中編譯代碼調試會很方便,直接點擊debug按鈕即可進行代碼調試,如果是混合工程在Android studio或者xcode中運行的工程,則沒辦法這么做,但也可以實現調試:將要調試的App安裝到手機中(安裝debug版本),連接電腦,執行如下命令,同步Flutter代碼到設備的宿主App中

$ cd flutterProjectPath/
$ flutter attach

執行完命令后會進行等待設備連接狀態,然后打開宿主App,進入Flutter頁面,看到如下信息提示則表示同步成功

zbdeMacBook-Pro:example zb$ flutter attach
Waiting for a connection from Flutter on MI 5X...
Done.
Syncing files to device MI 5X...                             1.2s

??  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on MI 5X is available at: http://127.0.0.1:54422/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".

打開http://127.0.0.1:54422可以查看調試信息,如有代碼改動可以按r來實時同步界面,如果改動沒有實時生效可以按R重新啟動Flutter應用。

JDFlutter熱更新實踐

大部分跨端框架,諸如React Native / Weex / H5等,基本都能做到隨時進行熱修復,并隨時上線,用于及時修復突發的在線問題,架構非常靈活。Flutter因其AOT的設計,預想會很難達到這種靈活度,但技術上仍具有一定的可行性,正如我們在之前的Flutter介紹文章中提到的,按照先有的API設計,是可以支持熱修復的,但僅限于Android。官方最新的架構上已經支持了熱修復架構,大家可以更新到1.2.1版本查看,但是官方的功能還比較弱,無法做到版本控制和回滾的靈活性,所以JDFlutter并沒有采用。我們可以首先一起看一下Google官方熱修復方案的設計原理:Flutter1.2.1 版本引入了 Dynamic Patch

為了更清楚的了解官方熱修復的原理和過程,我們需要首先深入了解Flutter的業務包結構和整體運行過程:

Flutter App的包結構

可以看到主體代碼集中在asset目錄中,除此之外還有少量Android端的框架java代碼及flutter so引擎庫外:

1、icudtl.dat

2、isolate_snapshot_data

3、isolate_snapshot_instr

Flutter包的初始化流程

Flutter頁面啟動時是如何加載這些代碼的呢?那就要從Flutter的初始化說起了,在頁面啟動前需要調用FlutterMain.startInitialization來做初始化:

可以看到該初始化是要求在主線程完成的,另外主要完成了以下三點:

  • 配置了一些環境數據,比如各個核心包的路徑,主要是提供給其他一些模塊全局調用
  • 檢查 asset 下 Flutter 包的完整性,主要是上面介紹的一些核心包,一旦缺少核心的一些庫,就會直接拋異常。開發過程中我們經常因為配置導致有些文件沒有打包進去,然后會直接 crash,就是在這里觸發的,具體代碼如下:


  • 解壓部分 asset 下的資源到 data 分區,以下是一些片段的代碼,那為什么要解壓呢?放在 asset 下也是可以通過 assetManager 讀取的。這里 google 應該是從性能角度要求解壓的,因為頻繁的使用 assetManager 讀取 asset 是很容易造成多線程阻塞的,一旦阻塞了將會導致整個 Flutter 業務全部無法渲染,所以需要解壓一些核心的資源庫,而不是解壓了所有的資源 (例如圖片就沒有解壓)

從代碼來看,先增加要解壓的核心庫的目錄,然后啟動 task 從 asset 中解壓庫到 data 分區對應 app 數據下的 app_flutter 目錄,以下是解壓后的目錄結構:

其中 res_timestamp 文件用于標記一些時間戳,算法比較固定,根據客戶端的安裝時間及 app 的 version code 生成,也就是說當用戶打開 Flutter 頁面后這個值就是固定的,如果有任何修改引擎會默認有變化,刪除現有 app_flutter 的包,重新解壓

運行原理

上面是對Flutter程序加載的分析,最終Flutter頁面顯示是需要呈現在原生組件Flutter View中的,這個組件會和底層Flutter Native View 進行綁定,并最終運行上面說到的data分區的Dart代碼來渲染UI。如果使用的是Flutter Activity,則默認Flutter View是全屏顯示,如需要定制頁面,需要自己設計Activity。

熱修復實驗

了解了這些,其實熱修復方案已經呼之欲出,替換原有解壓后的app_flutter包,殺進程,然后重新加載Flutter頁面即可。這里我們可以做個簡單的實驗:采用adb命令push一些修改過的并編譯的dart代碼到app_flutter目錄:

  • 先打開Flutter頁面,默認會加載asset下的包,并解壓到data分區
  • 修改一個Flutter工程,并編譯代碼,最終在工程目錄my_flutter/.android/Flutter/build/intermediates/flutter/release中看到打包生成的文件
  • 這么文件目錄中只有 flutter_assets 目錄和 isolate_snapshot_data 文件是包含業務代碼和圖片的,其他部分基本不會變化,所以我們這里要替換的目錄也就是這兩個,大家可以使用 adb push 命令將資源文件 push 到對應的 data 分區來做個實驗。
adb push my_flutter/.android/Flutter/build/intermediates/flutter/release/isolate_snapshot_data /data/data/app包名  /app_flutter
  • 關閉 Flutter 頁面,在 Task 中殺掉進程,回來后重新打開 Flutter 頁面,就能看到改動的效果,圖片資源是存放在 flutter_asset 目錄的,將圖片放到這個目錄,同樣能更新圖片

上面這個實驗,驗證了方案基本是可行的,但這里只是簡單替換,實際使用中替換還是有很多問題的。那 Google 官方是如何設計的呢?

Google熱修復設計
熱修復步驟

Flutter SDK 1.2.1中,Google提供了ResourceUpdater,用來做包的檢查和下載解壓。升級步驟如下:

  • 在頁面初始化時,檢查固定的下載更新目錄有沒有業務升級包,從代碼來看,必須在manifest中打開該功能,設置DynamicPatching

從邏輯上來看,只有在頁面 onResume 或者 App 重新開啟的時候會下載升級包,整體下載是通過 http 請求完成的,整體實現代碼大家可以參考 ResourceUpdater 中 DownloadTask 的實現部分,這里就不細說了。

  • 每次 init 的時候都會觸發檢查 data 分區的 app_flutter 包,如果不存在就會從 aaset 目錄解壓出來,而升級包的替換就是在這步完成的,按照邏輯會優先檢查升級目錄有沒有包存在,如果存在則優先從升級目錄解壓,如果不存在還是從 asset 目錄解壓;
  • 當然在檢查到有升級包時,會對升級包的一些配置做校驗,主要是 manifest.json 文件,里面會包含 buildNumber/baselineChecksum 字段,同時也會對"isolate_snapshot_data", "isolate_snapshot_instr", "flutter_assets/isolate_snapshot_data"等文件做 CRC32 校驗。


  • 升級后的版本時間戳是從配置的 manifest.json 文件中讀取 patchNumber 和文件下載時間確定的,完成文件覆蓋后會重新生成。

以下是升級包的大概路徑如下:


如何配置服務器

文章上部分介紹了怎么打開升級patch的功能,因升級涉及到服務端,那Google是怎么做到關聯到服務器的呢?其實原理比較簡單,需要配置客戶端的manifest文件的meta屬性,增加PatchServerURL,也就是我們服務的地址,以及下載模式PatchDownloadMode和加載模式PatchInstallMode,默認是ON_NEXT_RESTART(下次初始化時)

整體流程
存在的缺陷
  • 過于定制化,全部在引擎完成,很難適配一些特殊的需求定制;
  • 不支持現在比較主流的升級流程,諸如灰度和白名單等功能;
  • 版本號的維度不好控制,同時不能做版本回滾等操作。
JDFlutter如何實現熱修復
實現原理

JDFlutter的整體實現原理,其實和Google是一樣的,目前來看不修改引擎的前提下,只有這種方案最簡單,但是我們沒有使用Google的這套升級架構,默認關閉了patch功能,并框架之外實現了替換包和加載的邏輯,優點是整體兼容性更強、更靈活。1、服務端根據客戶端的唯一標識支持了白名單和灰度下發升級包;2、優化下載和替換流程。Flutter的升級包一般有4-5M,而且從網絡端獲取,失敗率較高,替換過程又涉及到文件操作,操作不當容易產生UI阻塞或者包異常。接入JDFlutter的客戶端下載包后,并不會直接替換文件,而是修改名稱后解壓到app_flutter目錄,等待業務頁面重新打開或者重新初始化時再修改成Flutter標準名稱的文件。這種操作不存在性能問題,另外會把舊版的文件備份,以便回滾代碼;3、同時并發運行的Flutter頁面較多,需避免因為升級出現一些中間狀態,使得業務或者頁面無法打開的情況;4、升級失敗或者下載后業務包有問題,出現無法加載的情況或者文件丟失的情況可以控制回滾代碼;5、線上出現大量異常后,可以指定對應的Flutter業務執行降級策略,讓該業務迅速降級到H5頁面。

熱修復規劃

未來,JDFlutter會繼續在熱修復方面進行探索和驗證,以滿足京東業務的快速發展需要。而針對目前的方案,我們思考了如下的優化點:Flutter業務包差量升級:現有的升級模式都是全量包覆蓋,即使壓縮后升級包還是很大,影響升級成功率及用戶流量,后續會采用一些diff工具,對比生成差量的patch,通過服務端下發后,在客戶端合并成完整包,但升級次數較多后會導致最終版本碎片化,需要做好版本之前的維護關系,難度較大。

升級后及時更新頁面:現有方案(包括標準google升級方案)沒有辦法做到下載業務包或者替換業務包后及時刷新頁面,需要restart進程后重新開啟才能刷新頁面。未來我們會優化引擎,通過釋放底層資源并重新加載,來完成隨時刷新頁面的功能。

未來展望

Google Flutter是非常出色的跨端開發技術,現在已經取得了長足的發展。社區生態和框架成熟度也正在快速追趕RN。相信不久的將來,Flutter+RN一定會成為跨端開發平臺的絕代雙驕。
原創:京東云技術新知
原文鏈接:https://www.cnblogs.com/jdclouddeveloper/p/11691543.html

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容