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

一、簡述

對于構建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

加入后的項目結構則多了三個文件,如下:
<img src="https://user-gold-cdn.xitu.io/2019/1/8/1682cc135da28a1b?w=676&h=650&f=jpeg&s=78998" width="360"/>

上述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依賴進行管理的

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


image

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

對于上面的基礎組件比如還可以進行更細粒度的劃分,不過不建議劃分太多,對于與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的打包后產物收集后的目錄結構如下:
<img src="https://user-gold-cdn.xitu.io/2019/1/8/1682cc135f9d81df?w=764&h=1182&f=jpeg&s=99554" width="360"/>

對于傳遞依賴的支持,我們知道單獨的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混合開發工程化整體流程

image

九、后序

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

作者簡介

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

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