Flutter包大小治理上的探索與實踐

1、背景

Flutter做爲一種全新的響應式、跨平臺、高性能的移動開發框架,在性能、穩定性和多端體驗一致上都有着較好的表現,自開源以來,已經受到愈來愈多開發者的喜好。隨着Flutter框架的不斷髮展和完善,業內愈來愈多的團隊開始嘗試並落地Flutter技術。不過在實踐過程當中咱們發現,Flutter的接入會給現有的應用帶來比較明顯的包體積增長。不管是在Android仍是在iOS平臺上,僅僅是接入一個Flutter Demo頁面,包體積至少要增長5M,這對於那些包大小敏感的應用來講實際上是很難接受的。html

對於包大小問題,Flutter官方也在持續跟進優化:前端

除了Flutter SDK內部或Dart實現的優化,咱們是否還有進一步優化的空間呢?答案是確定的。爲了幫助業務方更好的接入和落地Flutter技術,MTFlutter團隊對Flutter的包大小問題進行了調研和實踐,設計並實現了一套基於動態下發的包大小優化方案,瘦身效果也很是可觀。這裏分享給你們,但願對你們能有所幫助或者啓發。git

2、Flutter包大小問題分析

在Flutter官方的優化文檔中,提到了減小應用尺寸的方法:在V1.16.2及以上使用—split-debug-info選項(能夠分離出debug info);移除無用資源,減小從庫中帶入的資源,控制適配的屏幕尺寸,壓縮圖片文件。這些措施比較直接並容易理解,但爲了探索進一步瘦身空間並讓你們更好的理解技術方案,咱們先從瞭解Flutter的產物構成開始,而後再一步步分析有哪些可行的方案。github

2.1 Flutter產物介紹

咱們首先以官方的Demo爲例,介紹一下Flutter的產物構成及各部分佔比。不一樣Flutter版本以及打包模式下,產物有所不一樣,本文均以Flutter 1.9 Release模式下的產物爲準。shell

2.1.1 iOS側Flutter產物後端

圖1 Flutter iOS 產物組成示意圖

iOS側的Flutter產物主要由四部分組成(info.plist 比較小,對包體積的影響可忽略,這裏不做爲重點介紹),表格1中列出了各部分的詳細信息。緩存

表1 Flutter產物組成

2.1.2 Android側Flutter產物安全

圖2 Flutter Android 產物組成示意圖

Android側的Flutter產物總共5.16MB,由四部分組成,表格2中列出了各部分的詳細信息。微信

表2 Flutter Android產物組成

2.1.3 各部分產物的變化趨勢網絡

不管是Android仍是iOS,Flutter的產物大致能夠分爲三部分:

  1. Flutter引擎,該部分大小固定不變,但初始佔比較高。
  2. Flutter業務與框架,該部分大小隨着Flutter業務代碼的增多而逐漸增長。它是這樣的一個曲線:初始增加速度極快,隨着代碼增多,增加速度逐漸減緩,最終趨近線性增加。緣由是Flutter有一個Tree Shaking機制,從Main方法開始,逐級引用,最終沒有被引用的代碼,好比類和函數都會被裁剪掉。一開始引入Flutter以後隨便寫一個業務,就會大量用到Flutter/Dart SDK代碼,這樣初期Flutter包體積極速增長,可是過了一個臨界點,用戶包體積的增長就基本取決於Flutter業務代碼增量,不會增加得太快。
  3. Flutter資源,該部分初始佔比較小,後期增加主要取決於用到的本地圖片資源的多少,增加趨勢與資源多少成正比。

下圖3展現了Flutter各資源變化的趨勢:

圖3 Flutter各資源大小變化的趨勢圖

2.2 不一樣優化思路分析

上面咱們對Flutter產物進行了分析,接下來看一下官方提供的優化思路如何應用於Flutter產物,以及對應的困難與收益如何。

1.刪減法

Flutter引擎中包括了Dart、skia、boringssl、icu、libpng等多個模塊,其中Dart和skia是必須的,其餘模塊若是用不到卻是能夠考慮裁掉,可以帶來幾百k的瘦身收益。業務方能夠根據業務訴求自定義裁剪。

Flutter業務產物,由於Flutter的Tree Shaking機制,該部分產物從代碼的角度已是精簡過的,要想繼續精簡只能從業務的角度去分析。

Flutter資源中佔比較多的通常是圖片,對於圖片能夠根據業務場景,適當下降圖片分辨率,或者考慮替換爲網絡圖片。

2.壓縮法

由於不管是Android仍是iOS,安裝包自己已是壓縮包了,對Flutter產物再次壓縮的收益很低,因此該方法並不適用。

3.動態下發

對於靜態資源,理論上是Android和iOS均可以作到動態下發。而對於代碼邏輯部分的編譯產物,在Android平臺支持可執行產物的動態加載,iOS平臺則不容許執行動態下發的機器指令。

通過上面的分析能夠發現,除了刪減、壓縮,對全部業務適用、可行且收益明顯的進一步優化空間重點在於動態下發了。可以動態下發的部分越多,包大小的收益越大。所以咱們決定從動態下發入手來設計一套Flutter包大小優化方案。

3、基於動態下發的Flutter包大小優化方案

咱們在Android和iOS上實現的包大小優化方案有所不一樣,區別在於Android側能夠作到so和Flutter資源的所有動態下發,而iOS側因爲系統限制沒法動態下發可執行產物,因此須要對產物的組成和其加載邏輯進行分析,將其中非必須和動態連接庫一塊兒加載的部分進行動態下發、運行時加載。

當將產物動態下發後,還須要對引擎的初始化流程作修改,這樣才能保證產物的正常加載。因爲兩端技術棧的不一樣,在不少具體實現上都採用了不一樣的方式,下面就分別來介紹下兩端的方案。

3.1 iOS側方案

在iOS平臺上,因爲系統的限制沒法實如今運行時加載並運行可執行文件,而在上文產物介紹中能夠看到,佔比較高的App及Flutter這兩個均是可執行文件,理論上是不能進行動態下發的,實際上對於Flutter可執行文件咱們能作的確實很少,但對於App這個可執行文件,其內部組成的四個模塊並非在連接時都必須存在的,能夠考慮部分移出,進而來實現包體積的縮減。

所以,在該部分咱們首先介紹Flutter產物的生成和加載的流程,經過對流程細節的分析來挖掘出產物能夠被拆分出動態下發的部分,而後基於實現原理來設計實現工程化的方案。

3.1.1 實現原理簡析

爲了實現App的拆分,咱們須要瞭解下App.framework是怎樣生成以及各部分資源時如何加載的。以下圖4所示,Dart代碼會使用gen_snapshot工具來編譯成.S文件,而後經過xcrun工具來進行彙編和連接最終生成App.framework。其中gen_snapshot是Dart編譯器,採用了Tree Shaking等技術,用於生成彙編形式的機器代碼。

圖4 App.framework生成流程示意圖

產物加載流程:

圖5 Flutter產物加載流程圖

如上圖5所示,Flutter engine在初始化時會從根據 FlutterDartProject 的settings中配置資源路徑來加載可執行文件(App)、flutter_assets等資源,具體settings的相關配置以下:

// settings
{
...
  // snapshot 文件地址或內存地址
  std::string vm_snapshot_data_path;  
  MappingCallback vm_snapshot_data;
  std::string vm_snapshot_instr_path;  
  MappingCallback vm_snapshot_instr;

  std::string isolate_snapshot_data_path;  
  MappingCallback isolate_snapshot_data;
  std::string isolate_snapshot_instr_path;  
  MappingCallback isolate_snapshot_instr;

  // library 模式下的lib文件路徑
  std::string application_library_path;
  // icudlt.dat 文件路徑
  std::string icu_data_path;
  // flutter_assets 資源文件夾路徑
  std::string assets_path;
  // 
...
}


以加載vm_snapshot_data爲例,它的加載邏輯以下:

std::unique_ptr<DartSnapshotBuffer> ResolveVMData(const Settings& settings) {
  // 從 settings.vm_snapshot_data 中取
  if (settings.vm_snapshot_data) {
    ...
  }
  
  // 從 settings.vm_snapshot_data_path 中取
  if (settings.vm_snapshot_data_path.size() > 0) {
    ...
  }
  // 從 settings.application_library_path 中取
  if (settings.application_library_path.size() > 0) {
    ...
  }

  auto loaded_process = fml::NativeLibrary::CreateForCurrentProcess();
  // 根據 kVMDataSymbol 從native library中加載
  return DartSnapshotBuffer::CreateWithSymbolInLibrary(
      loaded_process, DartSnapshot::kVMDataSymbol);
}

對於iOS來講,它默認會根據kVMDataSymbol來從App中加載對應資源,而其實settings是給提供了經過path的方式來加載資源和snapshot入口,那麼對於 flutter_assets、icudtl.dat這些靜態資源,咱們徹底能夠將其移出託管到服務端,而後動態下發。

而因爲iOS系統的限制,整個App可執行文件則不能夠動態下發,但在第二部分的介紹中咱們瞭解到,其實App是由kDartVmSnapshotData、kDartVmSnapshotInstructions、kDartIsolateSnapshotData、kDartIsolateSnapshotInstructions等四個部分組成的,其中kDartIsolateSnapshotInstructions、kDartVmSnapshotInstructions爲指令段,不可經過動態下發的方式來加載,而kDartIsolateSnapshotData、kDartVmSnapshotData爲數據段,它們在加載時不存在限制。

到這裏,其實咱們就能夠獲得iOS側Flutter包大小的優化方案:將flutter_assets、icudtl.dat等靜態資源及kDartVmSnapshotData、kDartIsolateSnapshotData兩部分在編譯時拆分出去,經過動態下發的方式來實現包大小的縮減。但此方案有個問題,kDartVmSnapshotData、kDartIsolateSnapshotData是在編譯時就寫入到App中了,如何實現自動化地把此部分拆分出去是一個待解決的問題。爲了解決此問題,咱們須要先了解kDartVmSnapshotData、kDartIsolateSnapshotData的寫入時機。接下來,咱們經過下圖6來簡單地介紹一下該過程:

圖6 Flutter Data段寫入時序圖

代碼經過gen_snapshot工具來進行編譯,它的入口在gen_snapshot.cc文件,經過初始化、預編譯等過程,最終調用Dart_CreateAppAOTSnapshotAsAssembly方法來寫入snapshot。所以,咱們能夠經過修改此流程,在寫入snapshot時只將instructions寫入,而將data重定向輸入到文件,便可實現 kDartVmSnapshotData、kDartIsolateSnapshotData與App的分離。此部分流程示意圖以下圖7所示:

圖7 Flutter產物拆分流程示意圖

3.1.2 工程化方案

在完成了App數據段與代碼段分離的工做後,咱們就能夠將數據段及資源文件經過動態下發、運行時加載的方式來實現包體積的縮減。由此思路衍生的iOS側總體方案的架構以下圖8所示;其中定製編譯產物階段主要負責定製Flutter engine及Flutter SDK,以便完成產物的「瘦身」工做;發佈集成階段則爲產物的發佈和工程集成提供了一套標準化、自動化的解決方案;而運行階段的使命是保證「瘦身」的資源在engine啓動的時候能被安全穩定地加載。

圖8 架構設計

注:圖例中MTFlutterRoute爲Flutter路由容器,MWS指的是美團雲。

3.1.2.1 定製編譯產物階段

雖然咱們不能把App.framework及Flutter.framework經過動態下發的方式徹底拆分出去,但能夠剝離出部分非安裝時必須的產物資源,經過動態下發的方式來達到Flutter包體積縮減的目的,所以在該階段主要工做包括三部分。

1.新增編譯command

在將Flutter包瘦身工程化時,咱們必須保證現有的流程的編譯規則不會被影響,須要考慮如下兩點:

  • 增長編譯「瘦身」的Flutter產物構建模式, 該模式應能編譯出AOT模式下的瘦身產物。
  • 不對常規的編譯模式(debug、profile、release)引入影響。

對於iOS平臺來講,AOT模式Flutter產物編譯的關鍵工做流程圖以下圖9所示。runCommand會將編譯所需參數及環境變量封裝傳遞給編譯後端(gen_snapshot負責此部分工做),進而完成產物的編譯工做:

圖9 AOT模式Flutter產物編譯的關鍵工做流程圖

爲了實現「瘦身」的工做流,工具鏈在圖9的流程中新增了buildwithoutdata的編譯command,該命令針對經過傳遞相應參數(without-data=true)給到編譯後端(gen_snapshot),爲後續編譯出剝離data段提供支撐:

if [[ $# == 0 ]]; then
  # Backwards-compatibility: if no args are provided, build.
  BuildApp
else
  case $1 in
    "build")
      BuildApp ;;
    "buildWithoutData")
      BuildAppWithoutData ;;
    "thin")
      ThinAppFrameworks ;;
    "embed")
      EmbedFlutterFrameworks ;;
  esac
fi

..addFlag('without-data',
        negatable: false,
        defaultsTo: false,
        hide: true,
  )

2.編譯後端定製

該部分主要對gen_snapshot工具進行定製,當gen_snapshot工具在接收到Dart層傳來的「瘦身」命令時,會解析參數並執行咱們定製的方法Dart_CreateAppAOTSnapshotAsAssembly,該部分主要作了兩件事:

  1. 定製產物編譯過程,生成剝離data段的編譯產物。
  2. 重定向data段到文件中,以便後續進行使用。

具體處處理的細節,首先咱們須要在gen_sanpshot的入口處理傳參,並指定重定向data文件的地址:

CreateAndWritePrecompiledSnapshot() {
    ...
    if (snapshot_kind == kAppAOTAssembly) { // 常規release模式下產物的編譯流程
      ...
    } else if (snapshot_kind == kAppAOTAssemblyDropData) { 
      ...
      result = Dart_CreateAppAOTSnapshotAsAssembly(StreamingWriteCallback, 
                                                   file, 
                                                   &vm_snapshot_data_buffer,
                                                   &vm_snapshot_data_size,
                                                   &isolate_snapshot_data_buffer,
                                                   &isolate_snapshot_data_size,
                                                   true); // 定製產物編譯過程,生成剝離data段的編譯產物snapshot_assembly.S
      ...
    } else if (...) {
      ...
    }
    ...
  }

在接受到編譯「瘦身」模式的命令後,將會調用定製的FullSnapshotWriter類來實現Snapshot_assembly.S的生成,該類會將原有編譯過程當中vm_snapshot_data、isolate_snapshot_data的寫入過程改寫成緩存到buff中,以便後續寫入到獨立的文件中:

// drop_data=true, 表示後瘦身模式的編譯過程
// vm_snapshot_data_buffer、isolate_snapshot_data_buffer用於保存 vm_snapshot_data、isolate_snapshot_data以便後續寫入文件
Dart_CreateAppAOTSnapshotAsAssembly(Dart_StreamingWriteCallback callback,
                                    void* callback_data, 
                                    bool drop_data,
                                    uint8_t** vm_snapshot_data_buffer,
                                    uint8_t** isolate_snapshot_data_buffer) {
  ...
  FullSnapshotWriter writer(Snapshot::kFullAOT, &vm_snapshot_data_buffer,
                            &isolate_snapshot_data_buffer, ApiReallocate,
                            &image_writer, &image_writer);

  if (drop_data) {
    writer.WriteFullSnapshotWithoutData(); // 分離出數據段
  } else {
    writer.WriteFullSnapshot();
  }
  ...
}

當data段被緩存到buffer中後,即可以使用gen_snapshot提供的文件寫入的方法 WriteFile來實現數據段以文件形式從編譯產物中分離:

static void WriteFile(const char* filename, const uint8_t* buffer, const intptr_t size);
// 寫data到指定文件中
{
  ...
      WriteFile(vm_snapshot_data_filename, vm_snapshot_data_buffer, vm_snapshot_data_size); // 寫入vm_snapshot_data
      WriteFile(isolate_snapshot_data_filename, isolate_snapshot_data_buffer, isolate_snapshot_data_size); // 寫入isolate_snapshot_data
  ...
}

3.engine定製

編譯參數修改

iOS側使用-0z參數能夠得到包體積縮減的收益(大約爲700KB左右的收益),但會有相應的性能損耗,所以該部分做爲一個可選項提供給業務方,工具鏈提供相應版本的Flutter engine的定製。

資源加載方式定製

對於engine的定製,主要圍繞如何「手動」引入拆分出的資源來展開,好在engine提供了settings接口讓咱們能夠實現自定義引入文件的path,所以咱們須要作的就是對Flutter engine初始化的過程進行相應改造:

/**
 * custom icudtl.dat path
 */
@property(nonatomic, copy) NSString* icuDataPath;
​
/**
 * custom flutter_assets path
 */
@property(nonatomic, copy) NSString* assetPath;
​
/**
 * custom isolate_snapshot_data path
 */
@property(nonatomic, copy) NSString* isolateSnapshotDataPath;
​
/**
 *custom vm_snapshot_data path
 */
@property(nonatomic, copy) NSString* vmSnapshotDataPath;

在運行時「手動」配置上述路徑,並結合上述參數初始化FlutterDartProject,從而達到engine啓動時從配置路徑加載相應資源的目的。

engine編譯自動化

在完成engine的定製和改造後,還須要手動編譯一下engine源碼,生成各平臺、架構、模式下的產物,並將其集成到Flutter SDK中,爲了讓引擎定製的流程標準化、自動化,MTFlutter工具鏈提供了一套engine自動化編譯發佈的工具。如流程圖10所示,在完成engine代碼的自定義修改以後,工具鏈會根據engine的patch code編譯出各平臺、架構及不一樣模式下的engine產物,而後自動上傳到美團雲上,在開發和打包時只須要通簡單的命令,便可安裝和使用定製後的Flutter engine:

圖10 Flutter engine自動化編譯發佈流程

3.1.2.2 發佈集成階段

當完成Dart代碼編譯產物的定製後,咱們下一步要作的就是改造MTFlutter工具鏈現有的產物發佈流程,支持打出「瘦身」模式的產物,並將瘦身模式下的產物進行合理的組織、封裝、託管以方便產物的集成。從工具鏈的視角來看,該部分的流程示以下圖11所示:

圖11 Flutter產物發佈集成流程示意圖

自動化發佈與版本管理

MTFlutter工具鏈將「瘦身」集成到產物發佈的流水線中,新增一種thin模式下的產物,在iOS側該產物包括release模式下瘦身後的App.framework、Flutter.framework以及拆分出的數據、資源等文件。當開發者提交了代碼並使用Talos(美團內部前端持續交付平臺)觸發Flutter打包時,CI工具會自動打出瘦身的產物包及須要運行時下載的資源包、生成產物相關信息的校驗文件並自動上傳到美團雲上。對於產物資源的版本管理,咱們則複用了美團雲提供資源管理的能力。在美團雲上,產物資源以文件目錄的形式來實現各版本資源的相互隔離,同時對「瘦身」資源單獨開一個bucket進行單獨管理,在集成產物時,集成插件只需根據當前產物module的名稱及版本號即可獲取對應的產物。

自動化集成

針對瘦身模式MTFlutter工具鏈對集成插件也進行了相應的改造,以下圖12所示。咱們對Flutter集成插件進行了修改,在原有的產物集成模式的基礎上新增一種thin模式,該模式在表現形式與原有的debug、release、profile相似,區別在於:爲了方便開發人員調試,該模式會依據當前工程的buildconfigration來作相應的處理,即在debug模式下集成原有的debug產物,而在release模式下才集成「瘦身」產物包。

圖12 Flutter iOS端集成插件修改

3.1.2.3 運行階段

運行階段所處理的核心問題包括資源下載、緩存、解壓、加載及異常監控等。一個典型的瘦身模式下的engine啓動的過程如圖13所示。

該過程包括:

  • 資源下載:讀取工程配置文件,獲得當前Flutter module的版本,並查詢和下載遠程資源。
  • 資源解壓和校驗:對下載資源進行完整性校驗,校驗完成則進行解壓和本地緩存。
  • 啓動engine:在engine啓動時加載下載的資源。
  • 監控和異常處理:對整個流程可能出現的異常狀況進行處理,相關數據狀況進行監控上報。

圖13 iOS側瘦身模式下engine啓動流程圖

爲了方便業務方的使用、減小其接入成本,MTFlutter將該部分工做集成至MTFlutterRoute中,業務方僅需引入MTFlutterRoute便可將「瘦身」功能接入到項目中。

3.2 Android側方案

3.2.1 總體架構

在Android側,咱們作到了除Java代碼外的全部Flutter產物都動態下發。完整的優化方案歸納來講就是:動態下發+自定義引擎初始化+自定義資源加載。方案總體分爲打包階段和運行階段,打包階段會將Flutter產物移除並生成瘦身的APK,運行階段則完成產物下載、自定義引擎初始化及資源加載。其中產物的上傳和下載由DynLoader完成,這是由美團平臺迭代工程組提供的一套so與assets的動態下發框架,它包括編譯時和運行時兩部分的操做:

  1. 工程配置:配置須要上傳的so和assets文件。
  2. App打包時,會將配置1中的文件壓縮上傳到動態發佈系統,並從APK中移除。
  3. App每次啓動時,向動態發佈系統發起請求,請求須要下載的壓縮包,而後下載到本地並解壓,若是本地已經存在了,則不進行下載。

咱們在DynLoader的基礎上,經過對Flutter引擎初始化及資源加載流程進行定製,設計了總體的Flutter包大小優化方案:

圖14 Android側Flutter包大小優化方案總體架構

打包階段:咱們在原有的APK打包流程中,加入一些自定義的gradle plugin來對Flutter產物進行處理。在預處理流程,咱們將一些無用的資源文件移除,而後將flutter_assets中的文件打包爲bundle.zip。而後經過DynLoader提供的上傳插件將libflutter.so、libapp.so和flutter_assets/bundle.zip從APK中移除,並上傳到動態發佈系統託管。其中對於多架構的so,咱們經過在build.gradle中增長abiFilters進行過濾,只保留單架構的so。最終打包出來的APK即爲瘦身後的APK。

不經處理的話,瘦身後的APK一進到Flutter頁面確定會報錯,由於此時so和flutter_assets可能都還沒下載下來,即便已經下載下來,其位置也發生了改變,再使用原來的加載方式確定會找不到。因此咱們在運行階段須要作一些特殊處理:

1.Flutter路由攔截

首先要使用Flutter路由攔截器,在進到Flutter頁面以前,要確保so和flutter_assets都已經下載完成,若是沒有下載完,則顯示loading彈窗,而後調用DynLoader的方法去異步下載。當下載完成後,再執行原來的跳轉邏輯。

2.自定義引擎初始化

第一次進到Flutter頁面,須要先初始化Flutter引擎,其中主要是將libflutter.so和libapp.so的路徑改成動態下發的路徑。另外還須要將flutter_assets/bundle.zip進行解壓。

3.自定義資源加載

當引擎初始化完成後,開始執行Dart代碼的邏輯。此時確定會遇到資源加載,好比字體或者圖片。原有的資源加載器是經過method channel調用AssetManager的方法,從APK中的assets中進行加載,咱們須要改爲從動態下發的路徑中加載。

下面咱們詳細介紹下某些部分的具體實現。

3.2.2 自定義引擎初始化

原有的Flutter引擎初始化由FlutterMain類的兩個方法完成,分別爲startInitialization和ensureInitializationComplete,通常在Application初始化時調用startInitialization(懶加載模式會延遲到啓動Flutter頁面時再調用),而後在Flutter頁面啓動時調用ensureInitializationComplete確保初始化的完成。

圖15 Android側Flutter引擎初始化流程圖

在startInitialization方法中,會加載libflutter.so,在ensureInitializationComplete中會構建shellArgs參數,而後將shellArgs傳給FlutterJNI.nativeInit方法,由jni側完成引擎的初始化。其中shellArgs中有個參數AOT_SHARED_LIBRARY_NAME能夠用來指定libapp.so的路徑。

自定義引擎初始化,主要要修改兩個地方,一個是System.loadLibrary("flutter"),一個是shellArgs中libapp.so的路徑。有兩種辦法能夠作到:

  1. 直接修改FlutterMain的源碼,這種方式簡單直接,可是須要修改引擎並從新打包,業務方也須要使用定製的引擎才能夠。
  2. 繼承FlutterMain類,重寫startInitialization和ensureInitializationComplete的邏輯,讓業務方使用咱們的自定義類來初始化引擎。當自定義類完成引擎的初始化後,經過反射的方式修改sSettings和sInitialized,從而使得原有的初始化邏輯再也不執行。

本文使用第二種方式,須要在FlutterActivity的onCreate方法中首先調用自定義的引擎初始化方法,而後再調用super的onCreate方法。

3.2.3 自定義資源加載

Flutter中的資源加載由一組類完成,根據數據源的不一樣分爲了網絡資源加載和本地資源加載,其類圖以下:

圖16 Flutter 資源加載相關類圖

AssetBundle爲資源加載的抽象類,網絡資源由NetworkAssetBundle加載,打包到Apk中的資源由PlatformAssetBundle加載。

PlatformAssetBundle經過channel調用,最終由AssetManager去完成資源的加載並返回給Dart層。

咱們沒法修改PlatformAssetBundle原有的資源加載邏輯,可是咱們能夠自定義一個資源加載器對其進行替換:在widget樹的頂層經過DefaultAssetBundle注入。

自定義的資源加載器DynamicPlatformAssetBundle,經過channel調用,最終從動態下發的flutter_assets中加載資源。

3.2.4 字體動態加載

字體屬於一種特殊的資源,其有兩種加載方式:

  1. 靜態加載:在pubspec.yaml文件中聲明的字體及爲靜態加載,當引擎初始化的時候,會自動從AssetManager中加載靜態註冊的字體資源。
  2. 動態加載:Flutter提供了FontLoader類來完成字體的動態加載。

當資源動態下發後,assets中已經沒有字體文件了,因此靜態加載會失敗,咱們須要改成動態加載。

3.2.5 運行時代碼組織結構

整個方案的運行時部分涉及多個功能模塊,包括產物下載、引擎初始化、資源加載和字體加載,既有Native側的邏輯,也有Dart側的邏輯。如何將這些模塊合理的加以整合呢?平臺團隊的同窗給了很好的答案,並將其實現爲一個Flutter Plugin:flutter_dynamic(美團內部庫)。其總體分爲Dart側和Android側兩部分,Dart側提供字體和資源加載方法,方法內部經過method channel調到Android側,在Android側基於DynLoader提供的接口實現產物下載和資源加載的邏輯。

圖17 FlutterDynamic結構圖

4、方案的接入與使用

爲了讓你們瞭解上述方案使用層面的設計,咱們在此把美團內部的使用方式介紹給你們,其中會涉及到一些內部工具細節咱們暫不展開,重點解釋設計和使用體驗部分。因爲Android和iOS的實現方案有所區別,故在接入方式相應的也會有些差別,下面針對不一樣平臺分開來介紹:

4.1 iOS

在上文方案的設計中,咱們介紹到包瘦身功能已經集成進入美團內部MTFlutter工具鏈中,所以當業務方在使用了MTFlutter後只需簡單的幾步配置即可實現包瘦身功能的接入。iOS的接入使用上整體分爲三步:

1.引入Flutter集成插件(cocoapods-flutter-plugin 美團內部Cocoapods插件,進一步封裝Flutter模塊引入,使之更加清晰便捷):

gem 'cocoapods-flutter-plugin', '~> 1.2.0'

2.接入MTFlutterRoute混合業務容器(美團內部pod庫,封裝了Flutter初始化及全局路由等能力),實現基於「瘦身」產物的初始化:

Flutter 業務工程中引入 mt_flutter_route:

dependencies:
  mt_flutter_route: ^2.4.0

3.在iOS Native工程中引入MTFlutterRoute pod:

binary_pod 'MTFlutterRoute', '2.4.1.8'

通過上面的配置後,正常Flutter業務發版時就會自動產生「瘦身」後的產物,此時只需在工程中配置瘦身模式便可完成接入:

flutter 'your_flutter_project', 'x.x.x', :thin => true

4.2 Android

4.2.1 Flutter側修改

  1. 在Flutter工程pubspec.yaml中添加flutter_dynamic(美團內部Flutter Plugin,負責Dart側的字體、資源加載)依賴。
  2. 在main.dart中添加字體動態加載邏輯,並替換默認資源加載器。
void main() async {
   // 動態加載字體
  await dynFontInit();
  // 自定義資源加載器
  runApp(DefaultAssetBundle(
    bundle: dynRootBundle,
    child: MyApp(),
  ));
}

4.2.2 Native側修改

1.打包腳本修改

在App模塊的build.gradle中經過apply特定plugin完成產物的刪減、壓縮以及上傳。

2.在Application的onCreate方法中初始化FlutterDynamic。

3.添加Flutter頁面跳轉攔截。

在跳轉到Flutter頁面以前,須要使用FlutterDynamic提供的接口來確保產物已經下載完成,在下載成功的回調中來執行真正的跳轉邏輯。

class FlutterRouteUtil {
    public static void startFlutterActivity(final Context context, Intent intent) {
        FlutterDynamic.getInstance().ensureLoaded(context, new LoadCallback() {
            @Override
            public void onSuccess() {
              // 在下載成功的回調中執行跳轉邏輯
                context.startActivity(intent);
            }
        });
    }
}

備註:若是App有使用相似WMRoute之類的路由組件的話,能夠自定義一個UriHandler來統一處理全部的Flutter頁面跳轉,一樣在ensureLoaded方法回調中執行真正的跳轉邏輯。

4.添加引擎初始化邏輯

咱們須要重寫FlutterActivity的onCreate方法,在super.onCreate以前先執行自定義的引擎初始化邏輯。

public class MainFlutterActivity extends FlutterActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) 
      // 確保自定義引擎初始化完成
        FlutterDynamic.getInstance().ensureFlutterInit(this);
        super.onCreate(savedInstanceState);
    }
}

5、總結展望

目前,動態下發的方案已在美團內部App上線使用,Android包瘦身效果到達95%,iOS包瘦身效果達到30%+。動態下發的方案雖然能顯著減小Flutter的包體積,但其收益是經過運行時下載的方式置換回來的。當Flutter業務的不斷迭代增加時,Flutter產物包也會隨之不斷變大,最終致使需下載的產物變大,也會對下載成功率帶來壓力。後續,咱們還會探索Flutter的分包邏輯,經過將不一樣的業務模塊拆分來下降單個產物包的大小,來進一步保障包瘦身功能的可用性。

6、做者簡介

  • 豔東,2018年加入美團,到家平臺前端工程師。
  • 宗文,2019年加入美團,到家平臺前端高級工程師。
  • 會超,2014年加入美團,到家平臺前端技術專家。

招聘信息

美團外賣長期招聘Android、iOS、FE 高級/資深工程師和技術專家。歡迎感興趣的同窗投遞簡歷至:tech@meituan.com(郵件標題請註明:美團外賣技術團隊)。

閱讀更多技術文章,請掃碼關注微信公衆號-美團技術團隊!

相關文章
相關標籤/搜索