1. 程式人生 > >Flutter 混合工程持續整合實踐

Flutter 混合工程持續整合實踐

1. 引言</ font >

本文重點來講一講 Flutter 混合工程中的 Flutter 直接依賴解除的一些具體實現。

2. 思考</ font >

Flutter 和 Native 混合開發的模式,存在一部分同學只做 Native 開發,並不熟悉 Flutter 技術。

  1. 如果直接採用 Flutter 工程結構來作為日常開發,那這部分 Native 開發同學也需要配置 Flutter 環境,瞭解 Flutter 一些技術,成本比較大。

  2. 阿里集團的構建系統目前並不支援直接構建 Flutter 專案,這個也要求我們解除 Native 工程對 Flutter 的直接依賴。

鑑於這兩點原因,我們希望可以設計一個 Flutter 依賴抽取模組,可以將 Flutter 的依賴抽取為一個 Flutter 依賴庫釋出到遠端,供純 Native 工程引用。如下圖所示:
這裡寫圖片描述

Flutter直接依賴解除

3. 實現</ font >

3.1 Native工程依賴的Flutter分析

我們分析 Flutter 工程,會發現 Native 工程對 Flutter 工程的依賴主要有三部分:

  1. Flutter 庫和引擎:Flutter 的 Framework 庫和引擎庫;

  2. Flutter 工程:我們自己實現的 Flutter 模組功能,主要為 Flutter 工程下 lib 目錄下的 dart 程式碼實現的這部分功能;

  3. 自己實現的 Flutter Plugin:我們自己實現的 Flutter Plugin。

我們解開 Android 和 iOS 的 APP 檔案,發現 Flutter 依賴的主要檔案如下圖所示:

這裡寫圖片描述

Flutter依賴的檔案(Flutter產物)

其中,Android 的 Flutter 依賴的檔案:

  1. Flutter庫和引擎:icudtl.dat、libflutter.so、還有一些 class 檔案。這些都封裝在 flutter.jar 中,這個 jar 檔案位於 Flutter 庫目錄下的 flutter/bin/cache/artifacts/engine 下;

  2. Flutter工程產物:isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data、vm_snapshot_instr、flutter_assets;

  3. Flutter Plugin:各個plugin編譯出來的aar檔案。

其中:

  • isolate_snapshot_data:應用程式資料段;
  • isolate_snapshot_instr:應用程式指令段;
  • vm_snapshot_data:VM 虛擬機器資料段;
  • vm_snapshot_instr:VM 虛擬機器指令段;

iOS 的 Flutter 依賴的檔案:

  1. Flutter庫和引擎:Flutter.framework;

  2. Flutter工程的產物:App.framework;

  3. 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 目錄下能找到)。

  1. 增加 flutter.jar 的依賴。

  2. 插入 Flutter Plugin 的編譯依賴。

  3. 插入 Flutter 工程的編譯任務,最終將產物(兩個 isolaate_snapshot 檔案、兩個 vm_snapshot 檔案和 flutter_assets 資料夾)拷貝到 mergeAssets.outputDir,最終 merge 到 APK 的 assets 目錄下。

3.2.2 Android 的 Flutter 依賴抽取實現

弄明白 Flutter 工程的 Android 編譯產物之後,因此我們對 Android 的 Flutter 依賴抽取步驟如下:

  1. 編譯 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
  1. 將 flutter.jar 和 Flutter 工程的產物打包成一個 aar

這邊部分的主要工作是將 flutter.jar 和第 1 步編譯的產物封裝成一個 aar。

  1. 新增 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
        })
    }
}
  1. 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)
  1. 同時將這個 aar 和 Flutter Plugin 編譯出來的 aar 一起釋出到 maven 倉庫。
  1. 釋出 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
  1. 純粹的 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 與
    FLUTTERROOT/bin/cache/artifacts/engine/{FLUTTER_ROOT}/bin/cache/artifacts/engine/{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 版本號保持一致,便於後續問題追溯。

整個流程如下圖所示:
這裡寫圖片描述