7、Android.bp进阶:条件编译、预构建模块、默认模块与文件组
好,我们继续往下走。上一章我们把 Android.bp 的基础语法和常见模块类型过了一遍。这一章,咱们得聊点真正干活时绕不开的东西——条件编译、预构建、默认模块和文件组。
说实话,我刚接触 AOSP 那会儿,看到一堆 arch:、target: 的字段,第一反应是“这玩意儿真啰嗦”。后来在适配一款国产芯片时,被坑得够呛,才老老实实回来啃这些进阶用法。嗯,咱们今天就把这些坑填上。
7.1 条件编译:让同一个 .bp 适配不同平台
条件编译,说白了就是让构建系统根据当前编译的目标平台,自动选择不同的源文件、编译选项或者依赖库。你想想看,同一个模块要在 ARM 手机、x86 模拟器、甚至 Windows 主机上都能编译,代码肯定不能写死。
7.1.1 arch: 和 target: 字段
在 Android.bp 里,最常用的条件字段就是 arch 和 target。它们可以嵌套在几乎任何模块类型里。
cc_library {
name: "my_native_lib",
srcs: ["common.cpp"],
arch: {
arm: {
srcs: ["arm_neon_impl.cpp"],
cflags: ["-DUSE_NEON"],
},
x86: {
srcs: ["x86_sse_impl.cpp"],
cflags: ["-DUSE_SSE"],
},
arm64: {
srcs: ["arm64_impl.cpp"],
},
},
target: {
android: {
shared_libs: ["libcutils"],
},
host: {
static_libs: ["libbase"],
},
},
}
这里要注意,arch 是针对 CPU 架构的,target 是针对编译目标环境的(Android 设备还是主机工具)。我个人习惯把架构相关的优化代码放在 arch 里,把平台差异(比如依赖库不同)放在 target 里。
arch: { arm: { ... } } 里写了 srcs: ["arm_code.cpp"],但忘记把 common.cpp 里的默认实现用 #ifndef 保护起来。结果 ARM 编译时两个文件都编译了,符号冲突。记住:条件编译只控制哪些文件被加入编译列表,不控制头文件展开。
7.1.2 多条件组合与 os 字段
有时候你需要更精细的控制,比如“在 ARM64 的 Android 设备上”和“在 ARM64 的 Linux 主机上”行为不同。这时候可以用 target 里的 os 字段。
cc_binary {
name: "my_tool",
srcs: ["main.cpp"],
target: {
android_arm64: {
cflags: ["-DANDROID_ARM64"],
},
linux_arm64: {
cflags: ["-DLINUX_ARM64"],
},
},
}
注意 android_arm64 这种写法是 os_arch 的组合。支持的 os 值有 android、linux、darwin、windows 等。我在做 CTS 测试工具时,就靠这个区分了 Android 和 Linux 下的 socket API 差异。
7.2 预构建模块:拿来主义
预构建模块(prebuilt)是 AOSP 里非常实用的功能。说白了,就是你已经有一个编译好的 .so、.jar 或者 .apk,不想再重新编译,直接打包进系统镜像里。
7.2.1 prebuilt_shared_library 和 prebuilt_static_library
最常见的场景是集成第三方闭源库。比如某个芯片厂商提供的硬件抽象层库,只有 .so 文件,没有源码。
prebuilt_shared_library {
name: "libvendor_hw",
srcs: ["vendor/libvendor_hw.so"],
target: {
android_arm64: {
srcs: ["vendor/arm64/libvendor_hw.so"],
},
android_arm: {
srcs: ["vendor/arm/libvendor_hw.so"],
},
},
strip: {
none: true, // 预编译库通常不需要 strip
},
}
这里有个细节:srcs 在 prebuilt 模块里就是你要安装的二进制文件路径。你可以像普通模块一样用 arch 条件来指定不同架构的预编译文件。
strip: { none: true },否则构建系统默认会尝试 strip 符号表,可能会破坏第三方库的签名或校验。
7.2.2 prebuilt_jar 和 prebuilt_apk
Java 层的预构建也很常见。比如你有一个第三方 SDK 的 jar 包,或者一个预编译的 APK。
prebuilt_jar {
name: "my_sdk_jar",
srcs: ["libs/mysdk.jar"],
}
prebuilt_apk {
name: "MyPrebuiltApp",
srcs: ["vendor/apps/MyApp.apk"],
certificate: "platform", // 重新签名
privileged: true,
}
注意 prebuilt_apk 默认会使用测试签名,如果你需要系统权限,记得指定 certificate: "platform"。我曾经因为忘记这个,导致预装应用拿不到系统权限,折腾了两天。
7.3 默认模块:减少重复代码
默认模块(defaults)是 Android.bp 里最容易被忽视但最实用的功能。它相当于一个“配置模板”,你可以定义一组公共属性,然后在多个模块里引用。
7.3.1 定义和引用 defaults
cc_defaults {
name: "my_defaults",
cflags: ["-Wall", "-Werror"],
include_dirs: ["include"],
shared_libs: ["liblog", "libutils"],
}
cc_library {
name: "libfoo",
defaults: ["my_defaults"],
srcs: ["foo.cpp"],
}
cc_library {
name: "libbar",
defaults: ["my_defaults"],
srcs: ["bar.cpp"],
cflags: ["-DSPECIAL_FLAG"], // 可以追加,不会覆盖 defaults
}
你看,这样 libfoo 和 libbar 就不需要重复写那些公共的 cflags 和依赖了。而且子模块可以追加自己的配置,不会覆盖 defaults 里的值。
cflags),两者会合并成一个列表。但如果子模块重新定义了整个 shared_libs,则会完全替换 defaults 的值。这个细节我当年在 code review 时被怼过好几次。
7.3.2 多层继承
Defaults 还可以嵌套引用,形成继承链。
cc_defaults {
name: "base_defaults",
cflags: ["-DDEBUG"],
}
cc_defaults {
name: "vendor_defaults",
defaults: ["base_defaults"],
cflags: ["-DVENDOR"],
shared_libs: ["libvendor_support"],
}
cc_library {
name: "libvender_impl",
defaults: ["vendor_defaults"],
srcs: ["impl.cpp"],
}
最终 libvender_impl 的 cflags 会是 ["-DDEBUG", "-DVENDOR"]。我个人习惯把最基础的配置放在 base_defaults 里,产品线相关的放在二级 defaults 里,这样切换产品时只需要改一个地方。
7.4 文件组:逻辑上的文件集合
文件组(filegroup)是 Android.bp 里一个轻量但很实用的概念。它不产生任何编译输出,只是把一组文件打包成一个逻辑名字,供其他模块引用。
7.4.1 基本用法
filegroup {
name: "my_headers",
srcs: [
"include/foo.h",
"include/bar.h",
"include/internal/baz.h",
],
export_include_dirs: ["include"],
}
cc_library {
name: "libshared",
srcs: ["shared.cpp"],
header_libs: ["my_headers"],
}
你可能会问:“直接用 include_dirs 不就行了?” 嗯,区别在于 filegroup 可以跨模块复用,而且可以配合条件编译。比如你有一组头文件只在某个架构下需要,就可以用 arch 条件包裹 filegroup。
7.4.2 文件组与条件编译结合
filegroup {
name: "platform_specific_srcs",
srcs: [
"common_linux.cpp",
],
target: {
android: {
srcs: ["android_specific.cpp"],
},
host: {
srcs: ["host_specific.cpp"],
},
},
}
cc_binary {
name: "my_binary",
srcs: ["main.cpp", ":platform_specific_srcs"],
}
注意引用 filegroup 时要用 : 前缀。这个语法在 AOSP 里很常见,比如 ":my_headers"。我在重构一个大型模块时,用 filegroup 把分散在十几个目录下的源文件按功能分组,代码可读性提升了一大截。
srcs 支持 glob 模式(比如 srcs: ["src/**/*.cpp"]),但我不建议在 filegroup 里用 glob。因为 glob 会在构建时动态扫描目录,如果新增文件没有触发重新扫描,会导致漏编译。显式列出文件虽然麻烦,但更可靠。
7.5 知识体系总览
为了让你更直观地理解这些概念之间的关系,我画了一张图。你可以把它当作本章的“地图”。
7.6 实战建议
最后,我根据实际项目经验,给你几条建议:
- 条件编译别滥用:如果一个模块里
arch和target分支超过 5 个,说明你的代码设计可能有问题。考虑把平台相关代码抽成独立的库。 - 预构建模块一定要校验:拿到第三方 .so 后,先用
readelf -d检查依赖,确保所有依赖的符号在系统里都有。我遇到过预编译库依赖了libc++_shared.so但系统里没有的情况,运行时直接 crash。 - Defaults 命名要有规律:我习惯用
模块名_defaults或者产品线_defaults的格式,方便 grep 查找。 - Filegroup 是重构利器:当你觉得一个模块的
srcs列表太长时,别犹豫,拆成几个 filegroup 吧。
好了,这一章的内容就到这里。条件编译、预构建、默认模块和文件组,这四个工具用好了,你的 Android.bp 会变得既简洁又强大。下次再遇到多平台适配或者第三方库集成,你应该知道怎么下手了。
公众号:蓝海资料掘金营,微信deep3321