Flutter 混合工程持續整合實踐
1. 引言</ font >
本文重點來講一講 Flutter 混合工程中的 Flutter 直接依賴解除的一些具體實現。
2. 思考</ font >
Flutter 和 Native 混合開發的模式,存在一部分同學只做 Native 開發,並不熟悉 Flutter 技術。
-
如果直接採用 Flutter 工程結構來作為日常開發,那這部分 Native 開發同學也需要配置 Flutter 環境,瞭解 Flutter 一些技術,成本比較大。
-
阿里集團的構建系統目前並不支援直接構建 Flutter 專案,這個也要求我們解除 Native 工程對 Flutter 的直接依賴。
鑑於這兩點原因,我們希望可以設計一個 Flutter 依賴抽取模組,可以將 Flutter 的依賴抽取為一個 Flutter 依賴庫釋出到遠端,供純 Native 工程引用。如下圖所示:
Flutter直接依賴解除
3. 實現</ font >
3.1 Native工程依賴的Flutter分析
我們分析 Flutter 工程,會發現 Native 工程對 Flutter 工程的依賴主要有三部分:
-
Flutter 庫和引擎:Flutter 的 Framework 庫和引擎庫;
-
Flutter 工程:我們自己實現的 Flutter 模組功能,主要為 Flutter 工程下 lib 目錄下的 dart 程式碼實現的這部分功能;
-
自己實現的 Flutter Plugin:我們自己實現的 Flutter Plugin。
我們解開 Android 和 iOS 的 APP 檔案,發現 Flutter 依賴的主要檔案如下圖所示:
Flutter依賴的檔案(Flutter產物)
其中,Android 的 Flutter 依賴的檔案:
-
Flutter庫和引擎:icudtl.dat、libflutter.so、還有一些 class 檔案。這些都封裝在 flutter.jar 中,這個 jar 檔案位於 Flutter 庫目錄下的 flutter/bin/cache/artifacts/engine 下;
-
Flutter工程產物:isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data、vm_snapshot_instr、flutter_assets;
-
Flutter Plugin:各個plugin編譯出來的aar檔案。
其中:
- isolate_snapshot_data:應用程式資料段;
- isolate_snapshot_instr:應用程式指令段;
- vm_snapshot_data:VM 虛擬機器資料段;
- vm_snapshot_instr:VM 虛擬機器指令段;
iOS 的 Flutter 依賴的檔案:
-
Flutter庫和引擎:Flutter.framework;
-
Flutter工程的產物:App.framework;
-
Flutter Plugin:編譯出來的各種 plugin 的 framework,圖中的其他 framework;
那我們只需要將這三部分的編譯結果抽取出來,打包成一個 SDK 依賴的形式提供給 Native 工程,就可以解除 Native 工程對 Flutter 工程的直接依賴。
3.2 Android 依賴的 Flutter 庫抽取
3.2.1 Android 中 Flutter 編譯任務分析
Flutter 工程的 Android 打包,其實只是在 Android 的 Gradle 任務中插入了一個 flutter.gradle 的任務,而這個 flutter.gradle 主要做了三件事:(這個檔案可以在 Flutter 庫中的 flutter/packages/flutter_tools/gradle 目錄下能找到)。
-
增加 flutter.jar 的依賴。
-
插入 Flutter Plugin 的編譯依賴。
-
插入 Flutter 工程的編譯任務,最終將產物(兩個 isolaate_snapshot 檔案、兩個 vm_snapshot 檔案和 flutter_assets 資料夾)拷貝到 mergeAssets.outputDir,最終 merge 到 APK 的 assets 目錄下。
3.2.2 Android 的 Flutter 依賴抽取實現
弄明白 Flutter 工程的 Android 編譯產物之後,因此我們對 Android 的 Flutter 依賴抽取步驟如下:
- 編譯 Flutter 工程
這部分主要工作是編譯 Flutter 的 dart 和資源部分,可以用 AOT 和 Bundle 命令編譯。
echo "Clean old build"
find . -d -name "build" | xargs rm -rf
./flutter/bin/flutter clean
echo "Get packages"
./flutter/bin/flutter packages get
echo "Build release AOT"
./flutter/bin/flutter build aot --release --preview-dart-2 --output-dir=build/flutteroutput/aot
echo "Build release Bundle"
./flutter/bin/flutter build bundle --precompiled --preview-dart-2 --asset-dir=build/flutteroutput/flutter_assets
- 將 flutter.jar 和 Flutter 工程的產物打包成一個 aar
這邊部分的主要工作是將 flutter.jar 和第 1 步編譯的產物封裝成一個 aar。
- 新增 flutter.jar 依賴
project.android.buildTypes.each {
addFlutterJarImplementationDependency(project, releaseFlutterJar)
}
project.android.buildTypes.whenObjectAdded {
addFlutterJarImplementationDependency(project, releaseFlutterJar)
}
private static void addFlutterJarImplementationDependency(Project project, releaseFlutterJar) {
project.dependencies {
String configuration
if (project.getConfigurations().findByName("implementation")) {
configuration = "implementation"
} else {
configuration = "compile"
}
add(configuration, project.files {
releaseFlutterJar
})
}
}
- Merge Flutter 的產物到 assets
// merge flutter assets
def allertAsset = "${project.projectDir.getAbsolutePath()}/flutter/assets/release"
Task mergeFlutterAssets = project.tasks.create(name: "mergeFlutterAssets${variant.name.capitalize()}", type: Copy) {
dependsOn mergeFlutterMD5Assets
from (allertAsset){
include "flutter_assets/**" // the working dir and its files
include "vm_snapshot_data"
include "vm_snapshot_instr"
include "isolate_snapshot_data"
include "isolate_snapshot_instr"
}
into variant.mergeAssets.outputDir
}
variant.outputs[0].processResources.dependsOn(mergeFlutterAssets)
- 同時將這個 aar 和 Flutter Plugin 編譯出來的 aar 一起釋出到 maven 倉庫。
- 釋出 Flutter 工程產物打包的 aar
echo 'Clean packflutter input(flutter build)'
rm -f -r android/packflutter/flutter/
# 拷貝flutter.jar
echo 'Copy flutter jar'
mkdir -p android/packflutter/flutter/flutter/android-arm-release && cp flutter/bin/cache/artifacts/engine/android-arm-release/flutter.jar "$_"
# 拷貝asset
echo 'Copy flutter asset'
mkdir -p android/packflutter/flutter/assets/release && cp -r build/flutteroutput/aot/* "$_"
mkdir -p android/packflutter/flutter/assets/release/flutter_assets && cp -r build/flutteroutput/flutter_assets/* "$_"
# 將flutter庫和flutter_app打成aar 同時publish到Ali-maven
echo 'Build and publish idlefish flutter to aar'
cd android
if [ -n "$1" ]
then
./gradlew :packflutter:clean :packflutter:publish -PAAR_VERSION=$1
else
./gradlew :packflutter:clean :packflutter:publish
fi
cd ../
2) 釋出 Flutter Plugin 的 aar
# 將plugin釋出到Ali-maven
echo "Start publish flutter-plugins"
for line in $(cat .flutter-plugins)
do
plugin_name=${line%%=*}
echo 'Build and publish plugin:' ${plugin_name}
cd android
if [ -n "$1" ]
then
./gradlew :${plugin_name}:clean :${plugin_name}:publish -PAAR_VERSION=$1
else
./gradlew :${plugin_name}:clean :${plugin_name}:publish
fi
cd ../
done
- 純粹的 Native 專案只需要 compile 我們釋出到 maven 的 aar 即可。
平時開發階段,我們需要實時能依賴最新的 aar,所以我們採用 SNAPSHOT 版本。
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'}
ext {
flutter_aar_version = '6.0.2-SNAPSHOT'
}
dependencies {
//flutter主工程依賴:包含基於flutter開發的功能、flutter引擎lib
compile("com.taobao.fleamarket:IdleFishFlutter:${getFlutterAarVersion(project)}") {
changing = true
}
//...其他依賴
}
static def getFlutterAarVersion(project) {
def resultVersion = project.flutter_aar_version
if (project.hasProperty('FLUTTER_AAR_VERSION')) {
resultVersion = project.FLUTTER_AAR_VERSION
}
return resultVersion
}
3.3 iOS 依賴的 Flutter 庫的抽取
3.3.1 iOS 中 Flutter 依賴檔案如何產生
執行編譯命令 “flutter build ios”,最終會執行 Flutter 的編譯指令碼 xcode_backend.sh,而這個指令碼主要做了下面幾件事:
- 獲取各種引數(如 project_path,target_path,build_mode 等),主要來自於
Generated.xcconfig 的各種定義; - 刪除 Flutter 目錄下的 App.framework 和 app.flx;
- 對比 Flutter/Flutter.framework 與
{artifact_variant} 目錄下的
Flutter.framework ,若不相等,則用後者覆蓋前者; - 獲取生成 App.framework
命令所需引數(build_dir,local_engine_flag,preview_dart_2_flag,aot_flags); - 生成 App.framework,並將生成的 App.framework 和 AppFrameworkInfo.plist 拷貝到
XCode 工程的 Flutter 目錄下。
3.3.2 iOS 的 Flutter 依賴抽取實現
iOS 的 Flutter 依賴的抽取步驟如下:
- 編譯Flutter工程生成App.framework
echo "===清理flutter歷史編譯==="
./flutter/bin/flutter clean
echo "===重新生成plugin索引==="
./flutter/bin/flutter packages get
echo "===生成App.framework和flutter_assets==="
./flutter/bin/flutter build ios --release
- 打包各外掛為靜態庫
這裡主要有兩步:一是將 plugin 打成二進位制檔案,二是將 plugin 的註冊入口打成二進位制檔案。
echo "===生成各個plugin的二進位制庫檔案==="
cd ios/Pods
#/usr/bin/env xcrun xcodebuild clean
#/usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' BUILD_AOT_ONLY=YES VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner BUILD_DIR=../build/ios -sdk iphoneos
for plugin_name in ${plugin_arr}
do
echo "生成lib${plugin_name}.a..."
/usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' -target ${plugin_name} BUILD_DIR=../../build/ios -sdk iphoneos -quiet
/usr/bin/env xcrun xcodebuild build -configuration Debug ARCHS='x86_64' -target ${plugin_name} BUILD_DIR=../../build/ios -sdk iphonesimulator -quiet
echo "合併lib${plugin_name}.a..."
lipo -create "../../build/ios/Debug-iphonesimulator/${plugin_name}/lib${plugin_name}.a" "../../build/ios/Release-iphoneos/${plugin_name}/lib${plugin_name}.a" -o "../../build/ios/Release-iphoneos/${plugin_name}/lib${plugin_name}.a"
done
echo "===生成註冊入口的二進位制庫檔案==="
for reg_enter_name in "flutter_plugin_entrance" "flutter_service_register"
do
echo "生成lib${reg_enter_name}.a..."
/usr/bin/env xcrun xcodebuild build -configuration Release ARCHS='arm64 armv7' -target ${reg_enter_name} BUILD_DIR=../../build/ios -sdk iphoneos
/usr/bin/env xcrun xcodebuild build -configuration Debug ARCHS='x86_64' -target ${reg_enter_name} BUILD_DIR=../../build/ios -sdk iphonesimulator
echo "合併lib${reg_enter_name}.a..."
lipo -create "../../build/ios/Debug-iphonesimulator/${reg_enter_name}/lib${reg_enter_name}.a" "../../build/ios/Release-iphoneos/${reg_enter_name}/lib${reg_enter_name}.a" -o "../../build/ios/Release-iphoneos/${reg_enter_name}/lib${reg_enter_name}.a"
done
- 將這些上傳到遠端倉庫,並生成新的Tag。
- 純Native專案只需要更新pod依賴即可。
4. Flutter 混合工程的持續整合流程</ font >
按上述方式,我們就可以解除 Native 工程對 Flutter 工程的直接依賴了,但是在日常開發中還是存在一些問題:
- Flutter 工程更新,遠端依賴庫更新不及時;
- 版本整合時,容易忘記更新遠端依賴庫,導致版本沒有整合最新 Flutter 功能;
- 同時多條線並行開發 Flutter 時,版本管理混亂,容易出現遠端庫被覆蓋的問題;
- 需要最少一名同學持續跟進發布,人工成本較高;
鑑於這些問題,我們引入了我們團隊的 CI 自動化框架,從兩方面來解決:一方面是自動化,通過自動化減少人工成本,也減少人為失誤;另一方面是做好版本控制, 自動化的形式來做版本控制。
具體操作:
- 首先,每次需要構建純粹 Native 工程前自動完成 Flutter 工程對應的遠端庫的編譯釋出工作,整個過程不需要人工干預;
- 其次,在開發測試階段,採用五段式的版本號,最後一位自動遞增產生,這樣就可以保證測試階段的所有並行開發的 Flutter
庫的版本號不會產生衝突; - 最後,在釋出階段,採用三段式或四段式的版本號,可以和 APP 版本號保持一致,便於後續問題追溯。
整個流程如下圖所示: