Flutter 混合工程体系 一文中,阐述了Flutter 三种开发模式,在实际业务中搭建持续集成时,我们更希望发本地开发使用混合模式,持续集成使用解耦模式, 主要是解决以下两个问题:

  1. 混合模式:开发调试方便,包括热更新, Native 与 Flutter 开发源码断点调试
  2. 解耦模式:不侵入 Android /iOS Native工程,不对 Flutter 环境产生依赖,可以单独独立构建打包

我们这里重点阐述 解耦模式,要对 Android 与 Flutter 进行解藕,就需要分析混合模式最终生成的 APK 内容长啥样和 Flutter 构建脚本。

Android Flutter APK 文件结构分析

我们通过 Android Studio 菜单 Build -> Analyze APK 选择构建好的 Release APK 包,可以看到如下内容:

image.png

通过上面 APK 结构,我们会发现Flutter 项目比普通的Android 项目多了如下几个文件:

以下文件说明参考 https://blog.csdn.net/weixin_34001430/article/details/87942062

  • lib/armeabi-v7a/libflutter.so - Flutter 引擎
  • isolate_snapshot_instr - 应用程序指令段
  • isolate_snapshot_data - 应用程序数据段
  • vm_snapshot_instr - DartVM 指令段
  • vm_snapshot_data - DartVM 数据段
  • flutter_assets - 应用程序静态资源文件与配置

Flutter 项目 Gradle 构建脚本分析

在 Flutter Android 工程里面,我们在 build.gradle 文件里面会看到 Flutter gradle 构建脚本:

  1. apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

进入 flutter.gradle 里面,我们会看到相关 flutter 的打包构建流程,主要包括三个部分:

  • apply 方法:flutter 引擎平台架构(arm, x86) 库处理

image.png

  • buildBundle 方法:flutter aot 和 bundle 编译处理,也就是上面所说的 snapshot 相关flutter 构建产物文件

image.png

运行以后会发现其实执行的如下两条命令:

  1. // Debug
  2. flutter build bundle --debug
  3. // release
  4. flutter build aot --release
  • getAssets: 构建产物处理, 把 flutter snapshot 构建产物复制到 assets 目录,刚好与上面 APK 文件结构分析对应起来了。

image.png

Android Flutter 解耦处理

通过上面的 APK 文件和 Gradle 构建脚本分析,我们很清楚的知道了整个 Flutter 构建打包以及与Native 合并的过程, 接下来我们来进行 Android Native 工程 和 Flutter Module 工程解耦处理。

Android Native 项目依赖和构建配置处理

  • 移除 Android Native 项目 ${root}/setting.gradle 对 flutter 构建的脚本的依赖
  1. // 需要移除
  2. evaluate(new File(
  3. settingsDir.parentFile,
  4. 'happyflutter/.android/include_flutter.groovy'
  5. ))
  • 移除 Android Native 项目 ${root}/app/build.gradle 对 flutter 引擎库的依赖
  1. dependencies {
  2. implementation project(':flutter') // 需要删除
  3. }
  • Android Native 项目 ${root}/app/build.gradle 添加对 libs 文件夹的 .jar .aar 库
  1. dependencies {
  2. // implementation project(':flutter')
  3. implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
  4. }

Flutter Mobule 打包为独立的 .aar 文件 gradle 脚本编写

  • 修改 Flutter Module 项目构建类型 com.android.applicationcom.android.library
  • 简化 $flutterRoot/packages/flutter_tools/gradle/flutter.gradle 构建逻辑
  • ${root}/android/app/build.gradle 完整内容如下 ```bash import java.nio.file.Path import java.nio.file.Paths

apply plugin: ‘com.android.library’

android { compileSdkVersion 28

  1. compileOptions {
  2. sourceCompatibility 1.8
  3. targetCompatibility 1.8
  4. }
  5. defaultConfig {
  6. minSdkVersion 16
  7. targetSdkVersion 28
  8. versionCode 1
  9. versionName "1.0"
  10. testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

// ndk { // abiFilters ‘x86’, ‘x86_64’, ‘armeabi-v7a’, ‘armeabi-v8a’, ‘armeabi’ // } }

  1. buildTypes {
  2. profile {
  3. initWith debug
  4. }
  5. release {
  6. minifyEnabled false
  7. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  8. }
  9. }
  10. sourceSets {
  11. main {
  12. jniLibs.srcDirs "jniLibs"
  13. }
  14. }

}

// ${project.projectDir.getAbsolutePath()} Properties properties = new Properties() properties.load(project.rootProject.file(‘local.properties’).newDataInputStream()) String targetArch = ‘arm’ if (project.hasProperty(‘target-platform’) && project.property(‘target-platform’) == ‘android-arm64’) { targetArch = ‘arm64’ } // def isDebug = gradle.startParameter.taskNames.contains(“assembleDebug”) // def platform = isDebug ? “android-x86” : “android-${targetArch}-release” def flutterRoot = properties.getProperty(‘flutter.sdk’) def flutterJarPath = “${flutterRoot}/bin/cache/artifacts/engine/android-${targetArch}-release/flutter.jar”

Path baseEnginePath = Paths.get(flutterRoot, “bin”, “cache”, “artifacts”, “engine”) File debugFlutterJar = baseEnginePath.resolve(“android-${targetArch}”).resolve(“flutter.jar”).toFile() File releaseFlutterJar = baseEnginePath.resolve(“android-${targetArch}-release”).resolve(“flutter.jar”).toFile() File flutterX86Jar = baseEnginePath.resolve(“android-x86”).resolve(“flutter.jar”).toFile() File flutterX64Jar = baseEnginePath.resolve(“android-x64”).resolve(“flutter.jar”).toFile()

//println flutterX86Jar.length() //println flutterX86Jar.parentFile

Task flutterX86JarTask = project.tasks.create(“FlutterX86Jar”, Jar) { from(“${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so”) { into “lib/x86” } from(“${flutterRoot}/bin/cache/artifacts/engine/android-x64/libflutter.so”) { into “lib/x86_64” } }

project.android.buildTypes.each { addFlutterJarImplementationDependency(project, flutterX86JarTask, debugFlutterJar, releaseFlutterJar, flutterX86Jar, flutterX64Jar) } project.android.buildTypes.whenObjectAdded { addFlutterJarImplementationDependency(project, flutterX86JarTask, debugFlutterJar, releaseFlutterJar, flutterX86Jar, flutterX64Jar) }

private void addFlutterJarImplementationDependency(Project project, Task flutterX86JarTask, File debugFlutterJar, File releaseFlutterJar, File flutterX86Jar, File flutterX64Jar) { def isDebug = gradle.startParameter.taskNames.contains(“assembleDebug”) project.dependencies { String configuration if (project.getConfigurations().findByName(“implementation”)) { configuration = “implementation” } else { configuration = “compile” } add(configuration, project.files { if (isDebug) { [flutterX86JarTask, releaseFlutterJar] } else { releaseFlutterJar } }) } }

private Properties readPropertiesIfExist(File propertiesFile) { Properties result = new Properties() if (propertiesFile.exists()) { propertiesFile.withReader(‘UTF-8’) { reader -> result.load(reader) } } return result }

afterEvaluate { android.libraryVariants.all { def variant -> // println variant.mergeAssets.outputDir // println variant.buildType.debuggable

  1. def flutterAsset = files("../../build")
  2. def flutterAOT = files("../../build/aot")
  3. def mergeFlutterAssets = project.tasks.create(name: "mergeFlutterAssets${variant.name.capitalize()}", type: Copy) {
  4. // dependsOn copySharedFlutterAssetsTask
  5. dependsOn variant.mergeAssets
  6. from (flutterAsset){
  7. include "flutter_assets/**"
  8. exclude{
  9. details ->details.file.name.contains('isolate_snapshot_data') ||
  10. details.file.name.contains('vm_snapshot_data') ||
  11. details.file.name.contains('kernel_blob.bin')
  12. }
  13. }
  14. from (flutterAOT){
  15. include "vm_snapshot_data"
  16. include "vm_snapshot_instr"
  17. include "isolate_snapshot_data"
  18. include "isolate_snapshot_instr"
  19. }
  20. into variant.mergeAssets.outputDir
  21. }
  22. variant.outputs[0].processResources.dependsOn(mergeFlutterAssets)
  23. }

}

dependencies { implementation fileTree(dir: ‘libs’, include: [‘*.jar’]) implementation ‘com.android.support:appcompat-v7:28.0.0-alpha1’ testImplementation ‘junit:junit:4.12’ androidTestImplementation ‘com.android.support.test:runner:1.0.2’ androidTestImplementation ‘com.android.support.test.espresso:espresso-core:3.0.2’ }

  1. <a name="wNs99"></a>
  2. ### 编写 Flutter 插件和 Flutter Module 项目自动化 shell 脚本构建
  3. - 项目 AOT 和 Bundle 编译 ${root}/script/project.sh
  4. ```bash
  5. #!/bin/bash
  6. root=`pwd`
  7. . "${root}/script/util.sh"
  8. green "--root: ${root}, build mode: ${build}, run mode: ${run}"
  9. green "--start flutter aot and bundle build"
  10. flutter clean
  11. if [[ $build = "debug" ]]
  12. then
  13. # green ">>flutter build aot --debug && flutter build bundle --debug"
  14. # android-arm does not support AOT compilation
  15. # flutter build aot --debug
  16. # flutter build bundle --debug
  17. green "--android-arm does not support AOT compilation, use release build aot"
  18. yellow ">>flutter build aot --release && flutter build bundle --debug"
  19. # android-arm does not support AOT compilation
  20. flutter build aot --release
  21. flutter build bundle --debug
  22. if [[ $build = 'debug' ]] && [[ $run = 'simulator' ]]; then
  23. cp -f "${root}/build/flutter_assets/vm_snapshot_data" "${root}/build/aot/vm_snapshot_data"
  24. cp -f "${root}/build/flutter_assets/isolate_snapshot_data" "${root}/build/aot/isolate_snapshot_data"
  25. green ">>when debug and simulator mode, copy snapshot successfully!"
  26. fi
  27. yellow ">>./gradlew assembleDebug"
  28. cd android && ./gradlew clean && ./gradlew assembleDebug
  29. cd ..
  30. else
  31. yellow ">>flutter build aot --release && flutter build bundle --release"
  32. flutter build aot --release
  33. flutter build bundle --release
  34. yellow ">>./gradlew assembleRelease"
  35. cd android && ./gradlew clean && ./gradlew assembleRelease
  36. cd ..
  37. fi
  38. green "--flutter arr file[ ${root}/android/app/build/outputs/aar/app-${build}.aar ] build successfully!"
  39. cp -f "${root}/android/app/build/outputs/aar/app-${build}.aar" "${root}/publish/${build}"
  • Flutter 插件 .aar 文件生成和处理 ${root}/script/plugin.sh

这里是直接从 Flutter 插件编译的缓存目录拷贝 .arr 文件。目前需要自己手动在 Flutter Module 项目目录下执行 flutter build apk —debug flutter build apk —release 命令生成插件的 debug 和 release .arr 文件.

  1. #!/bin/bash
  2. root=`pwd`
  3. . "${root}/script/util.sh"
  4. echo "start flutter plugin aar copy......"
  5. for line in $(cat .flutter-plugins)
  6. do
  7. arr=(${line/=/ })
  8. name=${arr[0]}
  9. path=${arr[1]}
  10. plugin_name=`basename $path`
  11. plugin_dir=$(dirname $path)
  12. plugin=${path}android
  13. aar="${path}android/build/outputs/aar/${name}-${build}.aar"
  14. if [ ! -f $arr ]
  15. then
  16. cp -f $aar "${root}/publish/${build}"
  17. else
  18. echo "$aar is not exists"
  19. fi
  20. done
  21. echo "flutter plugin copy successfully!"
  • 拷贝 Flutter Module 生成的 .aar 文件给 Android Native 项目的 app/libs 目录 ${root}/script/copy.sh

HappyAndroid 为 Native 项目名称, Android Native 项目 和 Flutter Module 项目放到同一根目录下即可

  1. #!/bin/bash
  2. root=`pwd`
  3. . "${root}/script/util.sh"
  4. rm -rf "../HappyAndroid/app/libs/"
  5. cp -r "${root}/publish/${build}/" "../HappyAndroid/app/libs"

运行 shell 命令生成 .arr 文件 ${root}/script/build.sh

  1. #!/bin/bash
  2. root=`pwd`
  3. . "${root}/script/project.sh"
  4. . "${root}/script/plugin.sh"
  5. . "${root}/script/copy.sh"
  • 生成 debug .aar 模拟器包: ./script/build.sh build=debug
  • 生成 debug .aar 真机包: ./script/build.sh build=debug
  • 生成 release .aar 真机包: ./script/build.sh build=release

image.png

最后,按照正常的 Android Native 工程运行方式运行项目,即可 Running!

常见问题

打包模式 运行模式 构建 依赖包 可运行
Debug 模拟器
- flutter build aot —release
- flutter build bundle —debug
android-arm-release/flutter.jar

android-x64/flutter.jar

条件:复制 flutter_assets 里面的isolate_snapshot_data 和 vm_snapshot_data 覆盖 aot 里面的 isolate_snapshot_data 和 vm_snapshot_data
真机
- flutter build aot —release
- flutter build bundle —debug
android-arm-release/flutter.jar
真机
- flutter build aot —release
- flutter build bundle —release
android-arm-release/flutter.jar
真机
- flutter build aot —release
- flutter build bundle —debug
android-arm/flutter.jar
Error:JIT runtime cannot run a precompiled snapshot
真机
- flutter build aot —debug
- flutter build bundle —debug
android-arm/flutter.jar

android-arm-release/flutter.jar

android-arm does not support AOT compilation
Release 模拟器
真机
- flutter build aot —release
- flutter build bundle —release
android-arm-release/flutter.jar