Android多ProductFlavors能否复用同一externalNativeBuild的so库?
问题本质
Gradle 默认会为每一个ProductFlavor + BuildType组合生成独立的构建变体,每个变体会单独执行一套完整的externalNativeBuild流程。即使你的原生代码、编译参数和flavor完全无关,也会重复执行CMake配置、编译、链接全流程,构建耗时随flavor数量线性增长。
你的现有配置里已经把所有flavor的CMake构建输出目录设为同一路径,重复编译时产物会互相覆盖,但Gradle依然会按变体数量触发完整编译,属于完全无意义的耗时。
具体实现步骤
核心思路是将原生库编译逻辑和flavor构建流程解耦:原生库只按目标ABI编译一次,所有flavor打包时直接引用预编译好的so文件,彻底跳过重复的原生构建步骤。
1. 移除原有变体绑定的externalNativeBuild配置
删除defaultConfig块和android顶层下的所有externalNativeBuild相关配置,避免Gradle在每个flavor构建时自动触发CMake编译。
2. 配置全局jniLibs路径供所有flavor引用
在android配置块中添加sourceSets配置,指定所有构建变体统一从预编译目录读取so文件:
android { // 其他原有配置保持不变(applicationId、splits、productFlavors等) sourceSets { all { // 所有flavor、所有buildType都从该目录读取预编译so jniLibs.srcDirs = ["${RES_PATH}/proj/build/prebuilt-libs"] } } }
预编译目录需要遵循Android jniLibs的ABI规范结构:
${RES_PATH}/proj/build/prebuilt-libs/ ├── arm64-v8a/ │ └── libcocos.so └── armeabi-v7a/ └── libcocos.so
3. 编写独立的原生库编译Task
在app模块的build.gradle末尾添加独立编译任务,该任务不与任何flavor绑定,仅按需要的ABI编译一次原生库,同时支持增量编译——只有原生源码、CMake配置变更时才会重新执行,否则直接跳过:
task buildCocosNativeLibs { def cmakeVersion = "3.22.1" // 替换为你本地安装的CMake版本,和之前用的版本保持一致 def minSdkVersion = 21 // 替换为你项目实际的minSdk def cmakeBinPath = "${android.sdkDirectory}/cmake/${cmakeVersion}/bin/cmake" def ninjaBinPath = "${android.sdkDirectory}/cmake/${cmakeVersion}/bin/ninja" def stagingDir = file("${RES_PATH}/proj/build/cmake-staging") def prebuiltDir = file("${RES_PATH}/proj/build/prebuilt-libs") doLast { // 遍历你配置的两个目标ABI分别编译 ["arm64-v8a", "armeabi-v7a"].each { abi -> def abiBuildDir = file("${stagingDir}/${abi}") abiBuildDir.mkdirs() // 执行CMake配置 exec { workingDir abiBuildDir commandLine cmakeBinPath, "-H${projectDir}/../", "-B${abiBuildDir.absolutePath}", "-DANDROID_ABI=${abi}", "-DANDROID_PLATFORM=android-${minSdkVersion}", "-DANDROID_STL=c++_static", "-DANDROID_TOOLCHAIN=clang", "-DANDROID_ARM_NEON=TRUE", "-DANDROID_LD=gold", "-DRES_DIR=${RES_PATH}", "-DCOCOS_X_PATH=${COCOS_ENGINE_PATH}", "-DCMAKE_BUILD_TYPE=Release" // 打Debug包时替换为Debug } // 执行编译链接,用CPU核心数并行编译 exec { workingDir abiBuildDir commandLine ninjaBinPath, "-j${Runtime.runtime.availableProcessors()}", "cocos" } // 将编译好的so拷贝到预编译目录对应ABI路径 def abiPrebuiltDir = file("${prebuiltDir}/${abi}") abiPrebuiltDir.mkdirs() copy { from file("${abiBuildDir}/libcocos.so") into abiPrebuiltDir } } } // 配置增量编译规则:只有输入文件变化时才重新执行任务 inputs.dir(file(projectDir, "../Classes")) // 替换为你项目实际的C++源码存放路径 inputs.file(file(projectDir, "../CMakeLists.txt")) outputs.dir(prebuiltDir) }
4. 配置打包任务依赖(可选)
如果需要在执行多flavor打包时自动检查so是否存在、源码是否有更新,自动触发原生库编译,可以添加如下配置:
tasks.whenTaskAdded { task -> // 匹配所有变体的assemble任务 if (task.name.startsWith("assemble")) { task.dependsOn buildCocosNativeLibs } }
优化效果
配置完成后,原生库只会按目标ABI数量编译(你的场景下是2次,对应arm64-v8a和armeabi-v7a),6个flavor打包时直接复用已经编译好的so,不会再重复执行CMake流程。对于Cocos这类原生代码量较大的项目,整体构建耗时通常可以降低60%以上。
注意事项
- 如果需要区分Debug/Release的原生编译参数(比如Debug下开启调试符号、关闭优化),可以把编译任务拆分为
buildCocosNativeDebug和buildCocosNativeRelease两个,分别配置对应参数,再让对应buildType的assemble任务依赖对应任务即可。 - 如果你修改了原生代码或者CMakeLists.txt,增量编译机制会自动触发重新编译,不需要手动清理。
内容的提问来源于stack exchange,提问作者Mr.Hong

