微店的Flutter混合開發元件化與工程化架構

微店App技術團隊發表於2019-01-08

一、簡述

對於構建Flutter型別應用,因其開發語言Dart、虛擬機器、構建工具與平時我們開發Native應用不同且平臺虛擬機器也不支援,所以需要Flutter SDK來支援,如構建Android應用需要Android SDK一樣,下載Flutter SDK通常有兩種方式:

  1. 在官網下載構建好的zip包,裡面包含完整的Flutter基礎Api,Dart VM,Dart SDK等
  2. 手動構建,Clone Flutter原始碼後,執行flutter --packages get或其它具有檢測型別的命令如builddoctor,這時會自動構建和下載Dart SDK以及Flutter引擎產物

在我們微店App團隊的多人協作開發下,這種依賴每個開發本地下載Flutter SDK的方式,不能保證Flutter SDK的版本一致性與自動化管理,在開發時如果Flutter SDK版本不一致,往往會出現Dart層Api相容性或Flutter虛擬機器不一致等問題,因為每個版本的Flutter都有各自對應的Flutter虛擬機器,構建產物中會包含對應構建版本的虛擬機器。Flutter工程的構建需要Flutter標準的工程結構目錄和依賴於本地的Flutter環境,每個對應Flutter工程都有對應的Flutter SDK路徑,Android在local.properties中,IOS在Generated.xcconfig中,這個路徑會在Native工程本地依賴Flutter工程構建時讀取,並從中獲取引擎、資源和編譯構建Flutter工程,而呼叫flutter命令時構建Flutter工程則會獲取當前flutter命令所在的Flutter SDK路徑,並從中獲取引擎、資源和編譯構建Flutter工程,所以flutter命令構建環境與Flutter工程中平臺子工程的環境變數一定得保持一致,且這個環境變數是隨flutter執行動態改變的,團隊多人協作下這個得保證,在打包Flutter工程的正式版每個版本也應該有一個對應的Flutter構建版本,不管是本地打包還是在打包平臺打包。

我們知道Flutter應用的工程結構都與Native應用工程結構不一樣,不一致地方主要是Native工程是作為Flutter工程子工程,外層通過Pub進行依賴管理,這樣通過依賴下來的Flutter Plugin/Package程式碼即可與多平臺共享,在打包時Native子工程只打包工程程式碼與Pub所依賴庫的平臺程式碼,Flutter工程則通過flutter_tools打包lib目錄下以及Pub所依賴庫的Dart程式碼。回到正題,因工程結構的差異,如果基於現有的Native工程想使用Flutter來開發其中一個功能模組,一般來說混合開發至少得保證如下特點:

  1. 對Native工程無侵入
  2. 對Native工程零耦合
  3. 不影響Native工程的開發流程與打包流程
  4. 易本地除錯

顯然改變工程結構的方案可以直接忽略,官方也提供了一種Flutter本地依賴到現有Native的方案,不過這種方案不加改變優化而直接依賴的話,則會直接影響了其它無Flutter環境的開發同學的開發,影響開發流程,且打包平臺也不支援這種依賴方式的打包。

再講講Flutter SDK,平時進行Flutter開發過程中,難免避免不了因Flutter SDK的Bug亦或是需要改Flutter SDK中平臺連結的指令碼程式碼導致直接改動或者定製Flutter SDK,這種方式雖然可以解決問題或定製化,不過極其不推薦,這種方式對後續Flutter SDK的平滑升級極不友好,且帶來更多的後期維護成本。

接下來,本文主要是介紹如何對上述問題解決與實現:

  1. Flutter SDK版本一致性與自動化管理
  2. 無侵入Flutter SDK原始碼進行BugFix或定製化
  3. Flutter混合開發元件化架構
  4. Flutter混合開發工程化架構

二、Flutter四種工程型別

Flutter工程中,通常有以下幾種工程型別,下面分別簡單概述下: 1. Flutter Application 標準的Flutter App工程,包含標準的Dart層與Native平臺層 2. Flutter Module Flutter元件工程,僅包含Dart層實現,Native平臺層子工程為通過Flutter自動生成的隱藏工程 3. Flutter Plugin Flutter平臺外掛工程,包含Dart層與Native平臺層的實現 4. Flutter Package Flutter純Dart外掛工程,僅包含Dart層的實現,往往定義一些公共Widget

三、Flutter工程Pub依賴管理

Flutter工程之間的依賴管理是通過Pub來管理的,依賴的產物是直接原始碼依賴,這種依賴方式和IOS中的Pod有點像,都可以進行依賴庫版本號的區間限定與Git遠端依賴等,其中具體宣告依賴是在pubspec.yaml檔案中,其中的依賴編寫是基於YAML語法,YAML是一個專門用來編寫檔案配置的語言,下面是一個通過Git地址遠端依賴示例:

dependencies:
  uuid:
    git:
      url: git://github.com/Daegalus/dart-uuid.git
      ref: master
複製程式碼

宣告依賴後,通過執行flutter packages get命名,會從遠端或本地拉取對應的依賴,同時會生成pubspec.lock檔案,這個檔案和IOS中的Podfile.lock極其相似,會在本地鎖定當前依賴的庫以及對應版本號,只有當執行flutter packages upgrade時,這時才會更新,同樣pubspec.lock檔案也需要作為版本管理檔案提交到Git中,而不應gitignore。

1. Pub依賴衝突處理

對於PubPod這種依賴管理工具對於發生衝突時處理衝突的能力與Android的Gradle依賴管理相比差了一大截,所以當同一個庫發生版本衝突時,只能我們自己手動進行處理,而且隨著開發規模的擴大,肯定會出現傳遞依賴的庫之間的衝突。

Pub依賴衝突主要有兩種:

  1. 當前依賴庫的版本與當前的Dart SDK環境版本衝突
  2. 傳遞依賴時出現一個庫版本不一致衝突

第一種會在flutter packages get時報錯並提示為何出現衝突且最低需要的版本是多少,如下:

The current Dart SDK version is 2.1.0-dev.5.0.flutter-a2eb050044.

Because flutter_app depends on xml >=0.1.0 <3.0.1 which requires SDK version <2.0.0, version solving failed.                        
pub get failed (1)
複製程式碼

這個可以直接根據提示進行依賴庫的版本升級解決

而第二種則比較複雜點,假如有A、B、C三個庫,A和B都依賴C庫,如果A的某個版本依賴的C和B版本依賴的C版本不一致,則會發生衝突,而如何解決這種衝突呢?有兩種方式

1、首先把A和B庫的版本都設為any任意版本,如下:

dependencies:
	A: any
	B: any
複製程式碼

此時再通過flutter packages get時,則不會提示有版本衝突報錯,因為Pub已經自動選取了讓C庫版本一致的A、B庫的版本號,此時開啟同級目錄下的pubspec.lock檔案,搜尋A、B兩個庫,則會有對應無衝突的版本號,最後再把這兩個版本號分別替換掉any版本,這個版本衝突就解決了

2、通過版本覆蓋進行解決

2. Pub依賴版本覆蓋

Pub依賴管理中,既然支援傳遞依賴,同樣也提供了一種版本覆蓋的方式,意為強制指定一個版本,這和Android中Gradleforce有點相似,同樣版本覆蓋方式也可以用於解決衝突,如果知道某一個版本肯定不會衝突,則可直接通過版本覆蓋方式解決:

dependency_overrides:
  A: 2.0.0
複製程式碼

四、Flutter連結到Native工程原理

官方提供了一種本地依賴到現有的Native工程方式,具體可看官方wiki:Flutter本地依賴,這種方式太依賴於本地環境和侵入Native工程會影響其它開發同學,且打包平臺不支援這種方式的打包,所以肯定得基於這種方式進行優化改造,這個後面再說,先說說Native兩端本地依賴的原理

1. Android

在Android中本地依賴方式為:

  1. settings.gradle中注入include_flutter.groovy指令碼
  2. 在需要依賴的module中build.gradle新增project(':flutter')依賴

對於Android的本地依賴,主要是由include_flutter.groovyflutter.gradle這兩個指令碼負責Flutter的本地依賴和產物構建

1. include_flutter.groovy

settings.gradle中注入時,分別繫結了當前執行Gradle的上下文環境與執行include_flutter.groovy指令碼,該指令碼只做了下面三件事:

  1. include FlutterModule中的.android/Flutter工程
  2. include FlutterModule中的.flutter-plugins檔案中包含的Flutter工程路徑下的android module
  3. 配置所有工程的build.gradle配置執行階段都依賴於:flutter工程,也即它最先執行配置階段

其中.flutter-plugins檔案,是根據當前依賴自動生成的,裡面包含了當前Flutter工程所依賴(直接依賴和傳遞依賴)的Flutter子工程與絕對路徑的K-V關係,子工程可能是一個Flutter Plugin或者是一個Flutter Package,下面是.flutter-plugins中的一段內容示例: .flutter-plugins:

url_launcher=/Users/Sunzxyong/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-4.0.2/
複製程式碼

2. flutter.gradle

該指令碼位於Flutter SDK中,內容看起來很長,其實主要做了下面三件事:

  1. 選擇符合對應架構的Flutter引擎(flutter.so
  2. 解析上述.flutter-plugins檔案,把對應的android module新增到Native工程的依賴中(上述的include其實為這步做準備)
  3. Hook mergeAssets/processResources Task,預先執行FlutterTask,呼叫flutter命令編譯Dart層程式碼構建出flutter_assets產物,並拷貝到assets目錄下

有了上述三步,則直接在Native工程中執行構建即可自動構建Flutter工程中的程式碼並自動拷貝產物到Native中

2. IOS

在IOS中本地依賴方式為:

  1. 在Podfile中通過eval binding特性注入podhelper.rb指令碼,在pod install/update時會執行它
  2. 在IOS構建階段Build Phases中注入構建時需要執行的xcode_backend.sh指令碼

對於IOS的本地依賴,主要是由podhelper.rbxcode_backend.sh這兩個指令碼負責Flutter的Pod本地依賴和產物構建

1. podhelper.rb

因Podfile是通過ruby語言寫的,所以該指令碼也是ruby指令碼,該指令碼在pod install/update時主要做了三件事:

  1. Pod本地依賴Flutter引擎(Flutter.framework)與Flutter外掛登錄檔(FlutterPluginRegistrant)
  2. Pod本地原始碼依賴.flutter-plugins檔案中包含的Flutter工程路徑下的ios工程
  3. 在pod install執行完後post_install中,獲取當前target工程物件,匯入Generated.xcconfig配置,這些配置都為環境變數配置,主要為構建階段xcode_backend.sh指令碼執行做準備

上述事情即可保證Flutter工程以及傳遞依賴的都通過pod本地依賴進Native工程了,接下來就是構建了

2. xcode_backend.sh

該Shell指令碼位於Flutter SDK中,該指令碼主要就做了兩件事:

  1. 呼叫flutter命令編譯構建出產物(App.framework、flutter_assets)
  2. 把產物(*.framework、flutter_assets)拷貝到對應XCode構建產物中,對應產物目錄為:$HOME/Library/Developer/Xcode/DerivedData/${AppName}

上述兩個靜態庫*.framework是拷貝到${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app/Frameworks"目錄下

flutter_assets拷貝到${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app"目錄下

在XCode工程中,對應的是在${AppName}/Products/${AppName}.app

五、Flutter與Native通訊

Flutter與Native通訊有三種方式,這裡只簡單介紹下:

  1. MethodChannel:方法呼叫
  2. EventChannel:事件監聽
  3. BasicMessageChannel:訊息傳遞

Flutter與Native通訊都是雙向通道,可以互相呼叫和訊息傳遞

接下來是本文的重點內容,上述主要是普及下Flutter工程上比較重要的內容以及為下面要講做準備,當然還有打包模式、構建流程等就不放這裡了,後面可以單獨開一篇講

六、Flutter版本一致性與自動化管理

在團隊多人協作開發模式下,Flutter SDK的版本一致性與自動化管理,這是個必須解決的問題,通過這個問題,我們回看Android中Gradle的版本管理模式:

Gradle的版本管理是通過包裝器模式,每個Gradle專案都會對應一個Gradle構建版本,對應的Gradle版本在gradle-wrapper.properties配置檔案中進行配置,如果執行構建時本地沒有當前工程中對應的Gradle版本,則會自動下載所需的Gradle版本,而執行構建則是通過./gradlew包裝器模式進行執行,這樣本地配置的全域性Gradle環境與工程環境即可隔離開,對應的專案始終保持同一個Gradle版本的構建

這種包裝器模式的版本管理方式,可與每臺機器中全域性配置的環境保持隔離,在團隊多人協作下,也可保持同一個專案工程保持同一個構建版本

所以,我們沿用Gradle版本管理思想,在每個Flutter工程(包含上述說的四種工程)的根目錄加入三個檔案:

wrapper/flutter-wrapper.properties
flutterw
flutterw.bat
複製程式碼

加入後的專案結構則多了三個檔案,如下: 微店的Flutter混合開發元件化與工程化架構

上述flutter-wrapper.properties為當前工程Flutter SDK版本配置檔案,內容為:

distributionUrl=https://github.com/flutter/flutter
flutterVersion=1.0.0
複製程式碼

當然有需要可以再增加一些配置,目前這兩個配置已經足夠了,指定了Flutter的遠端地址以及版本號,如果Clone Github上專案比較慢,也可以改為私有維護的映象地址

flutterw為一個Shell指令碼,內部對版本管理主要做的事情為:

  1. 讀取配置的版本號,校驗Flutter SDK版本,不存在則觸發下載
  2. 更新Android中local.properties和IOS中Generated.xcconfig檔案中Flutter SDK地址
  3. 最後把命令列傳來的引數連結到Flutter SDK中的flutter進行執行

之後構建Flutter工程則用flutterw命令:

./flutterw build bundle
複製程式碼

而不用本地全域性配置的flutter命令,避免每個開發同學版本不一致問題,且這種方式對於新加入Flutter開發的同學來說,完全不需要自己手動下載Flutter SDK,只需執行一下flutterw任何命令,如./flutterw --version,即可自動觸發對應Flutter SDK的下載與安裝,實現優雅的自動化管理,這種方式對打包平臺來說也為支援Flutter工程的打包提供基礎

七、Flutter混合開發元件化架構

上述說的如果我們要利用Flutter來開發我們現有Native工程中的一個模組或功能,肯定得不能改變Native的工程結構以及不影響現有的開發流程,那麼,以何種方式進行混合開發呢? 前面說到Flutter的四種工程模型,Flutter App我們可以直接忽略,因為這是一個開發全新的Flutter App工程,對於Flutter Module,官方提供的本地依賴便是使用Flutter Module依賴到Native App的,而對於Flutter工程來說,構建Flutter工程必須得有個main.dart主入口,恰好Flutter Module中也有主入口

於是,我們進行元件劃分,通過Flutter Module作為所有通過Flutter實現的模組或功能的聚合入口,通過它進行Flutter層到Native層的雙向關聯。而Flutter開發程式碼寫在哪裡呢?當然可以直接寫在Flutter Module中,這沒問題,而如果後續開發了多個模組、元件,我們的Dart程式碼總不可能全部寫在Flutter Module中lib/吧,如果在lib/目錄下再建立子目錄進行模組區分,這不失為一種最簡單的方式,不過這會帶來一些問題,所有模組共用一個遠端Git地址,首先在元件開發隔離上完全耦合了,其次各個模組元件沒有單獨的版本號或Tag,且後續模組元件的增多,帶來更多的測試迴歸成本

正確的元件化方式為一個元件有一個獨立的遠端Git地址管理,這樣各個元件在發正式版時都有一個版本號和Tag,且在各個元件開發上完全隔離,後續元件的增多不影響其它元件,某個元件新增需求而不需迴歸其它元件,帶來更低的測試成本

前面提到Flutter Plugin可以有對應Dart層程式碼與平臺層的實現,所以可以這樣設計,一個元件對應一個Flutter Plugin,一個Flutter Plugin為一個完整的Flutter工程,有獨立的Git地址,而這些元件之間不能互相依賴,保持零耦合,所以這些元件都在業務層,可以叫做業務元件,這些業務元件之間的通訊和公共服務可以再劃分一層基礎層,可以叫做基礎元件,所有業務元件依賴基礎層,而Flutter Module作為聚合層依賴於所有Flutter元件,這些Flutter工程之間的依賴正是通過Pub依賴進行管理的

所以,綜合上述,整體的元件化架構可以設計為:

微店的Flutter混合開發元件化與工程化架構

業務元件與基礎元件的定位

對於上面的基礎元件比如還可以進行更細粒度的劃分,不過不建議劃分太多,對於與Native平臺層的通訊,每個業務元件對應一個Channel,當然內部還可以進行更細粒度的Channel進行劃分,這個Channel主要是負責Native層服務的提供,讓Flutter層消費。而對於Native層呼叫Flutter層的Api,應該儘可能少,需要調也只有出現一些值回撥時

因為Flutter的出現最本質的就是一次開發兩端執行,而如果有太多這種依賴於平臺層的實現,反而出現違背了,最後只是UI寫了一份而已。對於平臺層的實現也要儘量保持一個原則,即:

儘量讓Native平臺層成為服務層,讓Flutter層成為消費層呼叫Native層的服務,即Dart呼叫Native的Api,這樣當兩端開發人員編寫好一致基礎的服務介面後,Flutter的開發人員即可平滑使用和開發

而對於基礎元件中的公共服務元件Dart Api層的設計,因為公共服務主要呼叫Native層的服務,在Flutter中提供公共的Dart Api,作為Native到Flutter的一個橋樑,對於Native的服務,會有很有多種,而對應Api的設計為一個dart檔案對應一個種類的服務,整個公共服務元件提供一個統一個對外暴露的Dart,內部的細粒度的Dart實現通過export匯入,這種設計思想正是Flutter官方Api的設計,即統一對外暴露的Dart為common_service.dart

library common_service;

export 'network_plugin.dart';
export 'messager_plugin.dart';
...
複製程式碼

而上層業務元件呼叫Api只需要import一個dart即可,這樣對上層業務元件開發人員是透明的,上層不需要了解有哪些Api可用:

import 'package:common_service/common_service.dart';
複製程式碼

八、Flutter混合開發工程化架構

基本元件化的架構我們搭建好了,接下來是如何讓Flutter混合開發進行完整的工程化管理,我們都知道,對於官方的本地依賴這種方式,我們不能直接用,因為這會直接影響Native工程、開發流程與打包流程,所以我們得基於官方這種依賴方式進行優化改造,於是我們衍生出兩種Flutter連結到Native工程的方式:

  1. 本地依賴(原始碼依賴)
  2. 遠端依賴(產物依賴)

為什麼要有這兩種方式,首先本地依賴對於打包平臺不支援,現有打包平臺的環境,只能支援標準的Gradle工程結構進行打包,且本地依賴對於無需開發Flutter相關業務的同學來說是災難性的,所以便有了遠端依賴,遠端依賴直接依賴於打包好的Flutter產物,Android通過Gradle依賴,IOS通過Pod遠端依賴,這樣對其它業務開發同學來說是透明的,他們無需關心Flutter也不需要知道Flutter是否存在

對於這兩種依賴模式的使用環境也各不一樣

1. 本地依賴 本地依賴主要用於需要進行Flutter開發的同學,通過在對應Native工程中配置檔案配置是否開啟本地Flutter Module依賴,以及配置連結的本地Flutter Module地址,這樣Native工程即可自動依賴到本地的Flutter工程,整個過程是無縫的,同時本地依賴是通過原始碼進行依賴的,也可以很方便的進行Debug除錯 對於Android中配置檔案為本地的local.properties,IOS中為本地新建的local.xcconfig,兩個平臺的配置屬性保持一致:

FLUTTER_MODULE_LINK_ENABLE=true
FLUTTER_MODULE_LOCAL_LINK=/Users/Sunzxyong/FlutterProject/flutter_module
複製程式碼

2. 遠端依賴 遠端依賴是把Flutter Module的構成產物釋出到遠端,然後在Native工程中遠端依賴,這種依賴方式是預設的依賴方式,這樣對其它開發同學來說是透明的,不影響開發流程和打包平臺

上述說到的兩種依賴方式,接下來主要說怎麼進行這兩種依賴方式的工程化管理和定製化

1. 無侵入Flutter SDK原始碼進行BugFix和定製化

Flutter SDK在使用時,不免會遇到一些Flutter SDK的問題或Bug,但這些問題通常是在各平臺層的連結指令碼中出現坑,而如果我們要相容現有工程和擴充套件定製化功能,往往會直接修改Flutter SDK原始碼,這種侵入性的方式極不推薦,這對後續SDK的平滑升級會帶來更多的成本

通常出現Bug或需要定製化的指令碼往往是和平臺連結時相關的,當然排除需要修改dart層Api程式碼的情況下,這種只能更改原始碼了,不過這種出bug的機率還是比較小的,比較涉及到SDK的Api層面了。而大概率出現問題需要相容或進行定製化的幾個地方通常為下面幾處:

  1. $FLUTTER_SDK/packages/flutter_tools/gradle/flutter.gradle
  2. $FLUTTER_SDK/bin/cache/artifacts/engine/android-arch/flutter.jar
  3. $FLUTTER_MODULE/.android/build.gradle、.android/settings.gradle
  4. $FLUTTER_MODULE/.android/Flutter/build.gradle
  5. $FLUTTER_MODULE/.ios/Flutter/Generated.xcconfig
  6. $FLUTTER_MODULE/.ios/Flutter/podhelper.rb
  7. $FLUTTER_MODULE/.ios/Podfile
  8. $FLUTTER_SDK/packages/flutter_tools/bin/xcode_backend.sh

而我們需要相容的Flutter SDK的問題和定製化的點有下面幾項:

  1. Android:Flutter SDK中的Flutter引擎不支援armeabi架構
  2. Android:Flutter SDK中的flutter.gradle連結指令碼不支援非app名稱的Application工程
  3. Android:Flutter SDK中的flutter.gradle連結指令碼本地依賴存在flutter_shared資原始檔不拷貝Bug
  4. Android:解決上述幾項需要代理build.gradle構建指令碼,以及在build.gradle構建指令碼中定製化我們的構建產物收集Task
  5. IOS:Flutter Module中自動生成的.ios中的podhelper.rbruby指令碼使用了Pod中的post_install方法,導致Native工程不能使用或使用了的發生衝突,間接侵入了Native工程與耦合,限制性太強
  6. IOS:Flutter Module中自動生成的Podfile檔案,需要新增我們自己私有的Specs倉庫進行定製化
  7. IOS:解決post_install問題後,Flutter SDK中的xcode_backend.sh連結指令碼環境變數的讀取問題

為了實現無侵入Flutter SDK,對於上述的這些問題的解決,我們使用代理方式進行Bug的修改和定製化,下面是針對兩個平臺分別的實現策略

1. Android

在Android平臺上述問題和定製化的解決策略,對於armeabi架構的支援,我們可以通過指令碼進行自動化,上面講到flutterw的版本自動化管理,同樣,我們在裡面加段armeabi架構的支援指令碼,這樣做得好處是後續不需要支援了可以直接移除,通過呼叫./flutterw armeabi即可自動新增armeabi架構的引擎

對於Flutter SDK中的flutter.gradle連結指令碼的問題相容,不會直接在原始碼中進行更改,而是把它拷貝出來,命名為flutter_proxy.gradle,然後在代理指令碼中進行問題的修復,主要修復點為flutter_shared的支援與app硬編碼名稱的相容,如下:

		Task copySharedFlutterAssetsTask = project.tasks.create(name: "copySharedFlutterAssets${variant.name.capitalize()}", type: Copy) {
			from(project.zipTree(chosenFlutterJar))
			include 'assets/flutter_shared/*'
			into "src/${variant.name}"
		}
複製程式碼

再讓copyFlutterAssetsTask任務依賴於它,而app硬編碼名稱的相容,則更簡單了,通過在Native工程中local.properties配置Module名,再在flutter_proxy.gradle指令碼中加入讀取該屬性程式碼:

		String appName = loadRootProjectProperty(project, "FLUTTER_APP_NAME", "app")
		Task mergeAssets = project.tasks.findByPath(":${appName}:merge${variant.name.capitalize()}Assets")
複製程式碼

而對於build.gradle構建指令碼的代理,我們可以通過在執行Gradle構建時,通過-c命令進行settings.gradle的代理,進而代理掉build.gradle和指定Module中的build.gradle指令碼,如下:

cd .android
./gradlew assembleDebug -c ../script/proxy/settings.gradle
複製程式碼

而通過代理的settings.gradle檔案再進行build.gradle的代理:

getRootProject().buildFileName = 'build_proxy.gradle'
project(":flutter").buildFileName = "build_proxy.gradle"
複製程式碼

其中代理的Flutter/build.gradle中的指令碼apply會改為修復的Flutter SDK中的指令碼代理:

apply from: "${project.projectDir.parentFile.parentFile.absolutePath}/script/proxy/flutter_proxy.gradle"
複製程式碼

這樣.android工程在構建時期可以完全由我們自主控制,包括加入一些產物收集外掛、產物釋出到遠端外掛等定製功能

不過這種方式需要執行構建命令時手動指定代理指令碼,對於本地依賴時Native自動構建來說,是不會指定的,所有基於這種方式,我們再優化一下,因為Flutter Module.android.ios工程是通過Flutter SDK內部模版自動生成的,只要執行build|packages get等命令都會自動生成,首先想到是更改Flutter SDK內部工程模版,在Flutter SDK的packages/flutter_tools/templates目錄下,不過這與我們無侵入Flutter SDK違背了,所以不能選取這種方式

回想我們的Flutter SDK版本一致性管理是通過flutterw指令碼進行自動化的,而最終會連結呼叫到原生Flutter SDK中的命令,所以,我們可以在flutterw中加入指令碼,用於在.android.ios工程生成後,進行內部指令碼檔案的替換,把build.gradlesettings.gradle指令碼內容直接替換為我們的代理指令碼的內容,這樣既不侵入Flutter SDK,在後續維護起來也方便,後續不需要這個功能了,只需要把這段指令碼程式碼註釋就好了,隨即又恢復原生的構建指令碼了,flutterw指令碼執行過程如下:

function main() {
		# ...
		link_flutter "$@"
    	inject_proxy_build_script
    	# ...
}
複製程式碼

inject_proxy_build_script這個Shell函式會把對應指令碼進行我們的指令碼替換掉,當前函式內部也有對應判斷,因為flutterw主要用於Flutter SDK版本一致性管理,這裡僅對Flutter Module工程生效。所以這種方式不管是在本地依賴構建下還是通過命令列構建都可以完美支援

2. IOS

在IOS平臺上述問題和定製化的解決策略,對於IOS主要是對Podfilepodhelper.rb指令碼進行支援,而對Podfile的支援,這個比較簡單,在Podfile頭部通過指令碼注入我們自己私有的Specs倉庫即可:

source 'https://***/XXSpecs.git'
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
...
複製程式碼

這個工作同樣在flutterw執行後進行相容,後續不需要了可以直接註釋,這個自動注入指令碼也僅對Flutter Module工程生效

podhelper.rb指令碼的相容,主要是在進行本地依賴時,內部已經用了post_install函式,該函是在pod install後執行,這會與Native已經使用了該函式的發生衝突並報錯,所以我們通過flutterw指令碼的執行後預設註釋掉該指令碼中的post_install使用處,但是肯定不能平白無故註釋掉,我們要了解這段的作用,其實就是設定環境變數,為後續xcode_backend.sh指令碼的構建執行做準備,而註釋掉怎麼用另外一種方式恢復環境變數的設定這個後面再講,註釋後podhelper.rb指令碼程式碼片段為:

# post_install do |installer|
#     installer.pods_project.targets.each do |target|
#         target.build_configurations.each do |config|
#             config.build_settings['ENABLE_BITCODE'] = 'NO'
#             xcconfig_path = config.base_configuration_reference.real_path
#             File.open(xcconfig_path, 'a+') do |file|
#                 file.puts "#include \"#{File.realpath(File.join(framework_dir, 'Generated.xcconfig'))}\""
#             end
#         end
#     end
# end
複製程式碼

最終在flutterw自動支援上述處理指令碼執行流程為:

function main() {
		# ...
		link_flutter "$@"
    	# ...
    	podfile_support
    	podhelper_support
    	collect_ios_product "$@"
}
複製程式碼

函式內部判斷僅針對Flutter Module工程生效,畢竟其它Flutter Plugin工程不需要這種處理

2. 本地依賴無侵入流程

我們要做到只通過一個屬性配置檔案,在配置檔案中通過配置開發來開啟或關閉本地的Flutter Module連結依賴,只按官方的依賴方式肯定是不行的,不管是Android還是IOS,都會直接侵入Native工程,影響其它無Flutter環境同學的開發且影響打包平臺上的打包。所以,肯定得做優化,我們在官方這種依賴方式中加一層,作為代理層,而代理層主要做的工作是判斷本地是否有對應的屬性配置檔案且屬性值是否符合本地依賴Flutter Module的條件,如果是則進行本地Flutter Module的依賴,如果不是則Return掉,預設不做任何處理

所以通過這種代理方式即不影響Native工程原先的開發流程,對其它業務開發同學和打包平臺也是透明的

對於代理層的實現,Android與IOS平臺各不一樣

1. Android

Android是通過一個Gradle指令碼進行自動管理的,這個Gradle指令碼主要在settings.gradlebuild.gradle中做local.properties配置檔案的屬性值校驗,決定是否開啟本地Flutter Module連結的

2. IOS

IOS則較為複雜一些,因為涉及到Podfile中的ruby執行指令碼代理與Build Phases時期的Shell指令碼代理,所以得寫兩種型別的代理指令碼:Ruby和Shell,代理指令碼的最終執行還是會呼叫被代理的指令碼,只是在呼叫前做一層包裝邏輯判斷。而IOS中本身沒有本地配置檔案,所以我們新建一個IOS的本地配置檔案為local.xcconfig,這個配置檔案不隨版本進行管理,會gitignore掉,於是,在IOS中Podfile最終呼叫的指令碼是:

eval(File.read(File.join('./', 'FlutterSupport', 'podhelper_proxy.rb')), binding)
複製程式碼

而在Build Phases呼叫的是:

chmod +x "${SRCROOT}/FlutterSupport/xcode_backend_proxy.sh"
"${SRCROOT}/FlutterSupport/xcode_backend_proxy.sh" flutterBuild
複製程式碼

而剛剛上面說到的podhelper.rb指令碼中post_install函式被註釋掉後怎麼用另一種方式進行替換,我們知道這段函式主要就是提供在IOS構建階段時執行xcode_backend.sh的環境變數的,比如會獲取FLUTTER_ROOT等屬性值,這些環境變數由Flutter Module中Generated.xcconfig來提供,而如果我們把這個檔案的內容通過指令碼拷貝到IOS工程下對應構建配置的xcconfig中,如debug.xcconfigrelease.xcconfig,這種方式可行,不過會侵入Native工程,導致Native工程中多了這些變數,而且不優雅,我們要做到的是保證無侵入性

既然我們已經通過代理指令碼進行代理,那麼這些環境變數我們完全可以獲取出來,通過Shell指令碼的特性,子Shell會繼承於父Shell中export的環境變數值,所以,在代理Shell指令碼中再加段下面程式碼:

function export_xcconfig() {
	export ENABLE_BITCODE=NO
	if [[ $# != 0 ]]; then
		local g_xcconfig=$1/.ios/Flutter/Generated.xcconfig
		if [[ -f "$g_xcconfig" ]]; then
			# no piping.
			while read -r line
			do
  				if [[ ! "$line" =~ ^// ]]; then
					export "$line"
				fi
			done < $g_xcconfig
		fi
	fi
}
複製程式碼

其中注意不能使用管道,管道會在另外一個Shell程式

3. 遠端依賴產物打包流程

Flutter的遠端產物依賴,Android是通過Aar依賴,IOS是通過.a.framework靜態庫進行依賴,要進行這些遠端依賴很簡單,關鍵是如何打包獲取這些依賴的產物以及上傳到遠端,因為按照現有元件化的打包,除了聚合層Flutter Module中有對應的flutter-debug.aarApp.frameworkflutter_assets等產物的生成,其中業務元件和基礎元件中,也有對應的打包產物,這些打包產物會對應各自平臺打包不同型別產物,Android還是aar,而IOS則是.a靜態庫了,下面就分別講下Android與IOS的打包流程

1. Android

Android的打包比較簡單,通過在Flutter Module中的.android子工程下執行./gradlew assembleRelease,則會在對應Flutter中Android子工程的build目錄下輸出對應aar產物,而重點是怎麼獲取依賴的各元件(Flutter Plugin)中的產物,則是通過.flutter-plugins檔案,該檔案是在packages get時自動生成的,裡面包含了該Flutter工程通過Pub所依賴的庫,我們可以解析這個檔案,來獲取對應依賴庫的產物

2. IOS

IOS上的打包相比Android來說更復雜一些,我們藉助.ios/Runner來打包出靜態庫等產物,所以還需要設定簽名,通過在Flutter Module中直接執行./flutterw build ios --release,該命令會自動執行pod install,所以我們不必再單獨執行它,IOS中構建出的產物獲取也相對繁瑣些,除了獲取Flutter的相關產物,還需要獲取所依賴的各元件的靜態庫以及標頭檔案,需要獲取的產物如下:

Flutter.framework App.framework FlutterPluginRegistrant flutter_assets 所有依賴的Plugin的.a靜態庫以及標頭檔案

其中Flutter.framework為Flutter引擎,類似Android中的flutter.so,而App.framework則是Flutter中Dart編譯後的產物(Debug模式下它僅為一個空殼,具體Dart程式碼在flutter_assets中,Release模式下為編譯後的機器指令),FlutterPluginRegistrant是所有外掛Channel的登錄檔,也是自動生成的,flutter_assets含字型等資源,剩下一些.a靜態庫則是各元件在IOS平臺層的實現了

而收集IOS產物除了在.ios/Flutter目錄下收集*.framework靜態庫和flutter_assets外,剩下的就是收集.a靜態庫以及對應的標頭檔案了,而這些產物則是在構建Runner工程後,在Flutter Module下的

build/ios/$variant-iphoneos
複製程式碼

目錄下,variant對應所構建變體名,我們還是通過解析.flutter-plugins檔案,來獲取對應所依賴Flutter外掛的名稱,進而在上述的輸出目錄下找到對應的.a靜態庫,但是對應的標頭檔案而不在對應.a靜態庫目錄下,所以對於標頭檔案單獨獲取,因為解析了.flutter-plugins獲取到了KV鍵值對,對應的V則是該Flutter外掛工程地址,所以標頭檔案我們從裡面獲取

最後還需要獲取FlutterPluginRegistrant登錄檔的靜態庫以及標頭檔案

3. 產物收集與傳遞依賴

對於通過Flutter Module聚合層構建出來的產物,我們進行收集後再聚合到單獨的產物輸出目錄下,當然這一切都是通過指令碼自動做掉的

在Android上,通過Gradle外掛Hook assembleTask

		collectAarTask.dependsOn assembleTask
		assembleTask.finalizedBy collectAarTask
複製程式碼

這樣當執行完./gradlew assemble${variant}命令後則會自動進行產物收集

在IOS上,通過flutterw指令碼,在構建完後判斷構建命令是否是IOS構建命令,進而自動收集構建後的產物:

function collect_ios_product() {
	if [[ $# != 0 && $# > 2 ]]; then
		if [[ "$1" = "build" && "$2" = "ios" ]]; then
			# do collect...
		fi
	fi	
}		
複製程式碼

對應.a靜態庫和標頭檔案的收集關鍵指令碼程式碼如下:

		while read -r line
		do
			if [[ ! "$line" =~ ^// && ! "$line" =~ ^# ]]; then
				array=(${line//=/ })
				local library=$product_dir/${array[0]}/lib${array[0]}.a
				if [[ -f "$library" ]]; then
					local plugin=$dest_dir/plugins/${array[0]}
					rm -rf $plugin
					mkdir -p $plugin
					cp -f $library $plugin
					local classes=${array[1]}ios/Classes
					for header in `find "$classes" -name *.h`; do
						cp -f $header $plugin
					done
			else
				echo "The static library $library do not exist!"
				fi
			fi
		done < $flutter_plugins
複製程式碼

如下是Android與IOS的打包後產物收集後的目錄結構如下: 微店的Flutter混合開發元件化與工程化架構

對於傳遞依賴的支援,我們知道單獨的aar檔案以及通過podspec宣告這些靜態庫產物,是會丟失傳遞依賴的,丟失傳遞依賴可能導致我們Native工程中沒有使用到的一些三方庫,而Flutter工程中引用了,然後App執行Crash,而保證傳遞依賴的方式,則是Android釋出到遠端Maven,最後通過遠端依賴,上述產物只是本地依賴,IOS則是解析所有Flutter外掛中的podspec檔案,把它還原為JSON格式,通過解析dependencies物件,獲取對應的依賴庫命名以及版本號,最後在IOS遠端產物的podspec配置檔案中新增這些依賴

對於IOS的遠端依賴,我們知道單獨建一個獨立的Git倉庫就可以解決,通過配置好podspec,即可在IOS Native端進行遠端依賴,但是像Flutter.frameworkApp.framework這種大檔案,如果直接上傳到Git倉庫中有些不太友好,比如可以上傳到CDN中,然後通過podspecspec.prepare_command特性,在pod庫安裝時候預先執行一段指令碼把這兩個產物拉下來,對於目前來說,可以先傳到Git中,這樣比較直觀與可控,便於版本的管理

4. Flutter混合開發工程化整體流程

微店的Flutter混合開發元件化與工程化架構

九、後序

對於現有工程使用Flutter進行混合開發,坑點還是有的,比如效能、頁面棧管理等方面,只是目前還未踩到,加上目前Flutter上一些基礎庫不成熟,對於專案內的重要頁面以及動態化強度比較高的頁面,目前還是不建議使用Flutter進行開發,如果要使用也須做好降級方案,相反可以使用稍微輕量級點的頁面,且在設計時對於Flutter與Native層的通訊,應該讓Flutter作為消費層消費Native層提供的服務,Native端應做盡量少的改動,最好僅增加一處頁面路由的攔截器程式碼,在攔截器中通過Native與Flutter頁面的對映關係,把Native的頁面路由跳轉替換為Flutter頁面路由,這樣可以保證Native與Flutter的零耦合

作者簡介

zhengxiaoyong,@WeiDian,2016年加入微店,目前主要負責微店App的基礎支撐開發工作。

歡迎關注微店App技術團隊官方公眾號

微店App技術團隊

相關文章