21、Android.bp中的条件编译:arch、target、host、sdk_version、product_variables
条件编译,说白了就是让同一份代码,在不同环境下编译出不同的结果。
我在刚接触AOSP时,觉得这东西可有可无。直到有一次,我在x86模拟器上跑得好好的模块,一放到真机上就崩了。查了半天,原来是调了一个arm平台才有的API。嗯,从那以后,我再也不敢忽视条件编译了。
21.1 为什么需要条件编译?
Android的构建环境非常复杂。你想想看:
- 有不同架构:arm、arm64、x86、x86_64
- 有不同角色:target(目标设备)和host(编译主机)
- 有不同SDK版本:从低版本到高版本
- 还有不同产品:手机、平板、车机、电视
如果没有条件编译,你就得为每种组合写一份Android.bp。那维护成本,想想都头疼。
核心思想:条件编译让一份Android.bp文件,能适配多种构建场景。它通过arch、target、host、sdk_version、product_variables这些关键字,实现精准控制。
21.2 arch:架构相关的编译控制
arch是最常用的条件编译手段。它用来区分不同CPU架构。
cc_library {
name: "my_lib",
srcs: ["common.cpp"],
arch: {
arm: {
srcs: ["arm_specific.cpp"],
cflags: ["-DARM_OPTIMIZATION"],
},
arm64: {
srcs: ["arm64_specific.cpp"],
cflags: ["-DARM64_OPTIMIZATION"],
},
x86: {
srcs: ["x86_specific.cpp"],
},
x86_64: {
srcs: ["x86_64_specific.cpp"],
},
},
}
这段代码的意思是:
- common.cpp是所有架构都编译的
- arm架构下,额外编译arm_specific.cpp,并定义ARM_OPTIMIZATION宏
- arm64、x86、x86_64各自编译自己的专属文件
我个人习惯把架构相关的代码单独放在一个目录里,比如src/arch/arm/、src/arch/x86/。这样Android.bp里引用起来也清晰。
小技巧:arch内部还可以嵌套arch: { }。比如arm下再分armv7-a和armv8-a。但我不建议嵌套太深,否则可读性会变差。
21.3 target和host:目标设备 vs 编译主机
target和host是另一个维度。target指最终运行代码的设备,host指编译代码的机器。
举个例子:你写了一个编译工具,它只在编译时运行,不会跑到手机上。那这个工具就应该用host编译。
cc_binary {
name: "my_tool",
srcs: ["tool.cpp"],
target: {
android: {
srcs: ["android_specific.cpp"],
cflags: ["-DANDROID"],
},
host: {
srcs: ["host_specific.cpp"],
cflags: ["-DHOST"],
},
},
}
这里要注意:
- target: { android: { ... } } 表示编译到Android设备上的代码
- target: { host: { ... } } 表示编译到主机上的代码
- 如果不写target,默认就是target android
我曾经犯过一个错误:把一个只在host上用的工具,写到了target android里。结果编译出来的二进制文件,在Linux主机上根本跑不起来。嗯,从那以后,我每次写host工具都会检查一下target设置。
注意:host和target不能同时为true。一个模块要么是host,要么是target,不能既是编译工具又是设备程序。
21.4 sdk_version:SDK版本的条件编译
sdk_version用来控制不同Android API级别下的编译行为。这在处理兼容性时特别有用。
cc_library {
name: "my_compat_lib",
srcs: ["base.cpp"],
sdk_version: "30",
target: {
android: {
sdk_version: "31",
srcs: ["api31_specific.cpp"],
},
},
}
这段代码的意思是:
- 默认使用SDK 30编译
- 如果目标设备的SDK版本是31或以上,额外编译api31_specific.cpp
我个人习惯把sdk_version和arch结合起来用。比如:
cc_library {
name: "my_lib",
srcs: ["common.cpp"],
arch: {
arm64: {
sdk_version: "31",
srcs: ["arm64_api31.cpp"],
},
},
}
这样就能精确控制:只在arm64且SDK>=31时,才编译某些代码。
避坑指南:sdk_version的值必须是数字字符串,比如"30"、"31"。不要写成"android-30"或"API 30"。我曾经被这个坑过,编译报错半天找不到原因。
21.5 product_variables:产品级别的条件编译
product_variables是最灵活的条件编译方式。它允许你根据产品变量来控制编译行为。
产品变量是什么?就是你在BoardConfig.mk或product.mk里定义的那些变量,比如TARGET_PRODUCT、TARGET_DEVICE等。
cc_library {
name: "my_product_lib",
srcs: ["common.cpp"],
product_variables: {
eng: {
srcs: ["eng_debug.cpp"],
cflags: ["-DENG_BUILD"],
},
userdebug: {
srcs: ["userdebug_debug.cpp"],
},
user: {
// user版本不编译任何调试代码
},
},
}
这段代码的意思是:
- eng版本(工程师版本)编译eng_debug.cpp,并定义ENG_BUILD宏
- userdebug版本编译userdebug_debug.cpp
- user版本(用户版本)不编译任何调试代码
product_variables的强大之处在于,你可以自定义变量。比如在product.mk里定义:
PRODUCT_VARIABLES += MY_FEATURE_ENABLED
然后在Android.bp里:
cc_library {
name: "my_feature_lib",
srcs: ["common.cpp"],
product_variables: {
MY_FEATURE_ENABLED: {
srcs: ["feature_enabled.cpp"],
},
},
}
这样,只有定义了MY_FEATURE_ENABLED的产品,才会编译feature_enabled.cpp。
注意:product_variables的变量名必须全部大写,且只能包含字母、数字和下划线。这是Soong的硬性要求,不符合规则的变量会被忽略。
21.6 条件编译的组合使用
实际项目中,这些条件编译方式经常组合使用。我画了一张图,帮你理清它们的关系:
从这张图可以看出,条件编译的五个维度是相互独立的。你可以同时使用arch和sdk_version,也可以同时使用target和product_variables。
举个例子,一个真实项目中的组合使用:
cc_library {
name: "my_complex_lib",
srcs: ["base.cpp"],
// 默认编译所有架构
arch: {
arm64: {
srcs: ["arm64_opt.cpp"],
},
},
// 只在target android上编译
target: {
android: {
srcs: ["android_only.cpp"],
},
},
// SDK版本控制
sdk_version: "30",
// 产品变量控制
product_variables: {
eng: {
srcs: ["eng_debug.cpp"],
},
},
}
这段代码的意思是:
- base.cpp在所有情况下都编译
- arm64_opt.cpp只在arm64架构下编译
- android_only.cpp只在target android下编译
- 默认使用SDK 30
- eng_debug.cpp只在eng版本下编译
核心原则:条件编译的粒度越细,代码的可维护性越差。我建议你尽量用粗粒度控制,比如按架构分文件,而不是在同一个文件里写一堆#ifdef。这样代码更清晰,也更容易调试。
21.7 常见问题与避坑指南
我在项目中遇到过不少条件编译的坑,这里分享几个:
- arch和target的优先级:arch的优先级高于target。如果你在arch: { arm: { ... } }里写了target: { host: { ... } },那host设置会被忽略。因为arch已经限定了arm架构,target再写host就没意义了。
- sdk_version的默认值:如果不写sdk_version,默认使用当前产品的PLATFORM_SDK_VERSION。这个值在不同产品上可能不同,所以最好显式指定。
- product_variables的变量名:必须是全大写,且只能包含字母、数字和下划线。我见过有人用MY-FEATURE(带连字符),结果编译时直接报错。
- 条件编译的嵌套:虽然支持嵌套,但我不建议超过两层。比如arch: { arm: { sdk_version: "31" } } 还可以接受,但再往里嵌套product_variables就太复杂了。
重要提醒:条件编译不是万能的。如果某个模块在不同平台上的差异太大,我建议你直接写多个Android.bp文件,或者用不同的模块名。比如my_lib_arm和my_lib_x86,而不是在一个模块里写满条件编译。这样更清晰,也更容易维护。
好了,条件编译的内容就讲到这里。记住一句话:条件编译是为了简化,不是为了复杂。能用简单方式解决的问题,就不要用复杂的条件编译。
公众号:蓝海资料掘金营,微信deep3321