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 条件编译的组合使用

实际项目中,这些条件编译方式经常组合使用。我画了一张图,帮你理清它们的关系:

Android.bp 条件编译体系 Android.bp 模块 arch target host sdk_version product_variables arm arm64 x86 x86_64 android host "30" "31" "32" eng userdebug user 自定义变量 条件编译可以组合使用,实现精准控制

从这张图可以看出,条件编译的五个维度是相互独立的。你可以同时使用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 常见问题与避坑指南

我在项目中遇到过不少条件编译的坑,这里分享几个:

  1. arch和target的优先级:arch的优先级高于target。如果你在arch: { arm: { ... } }里写了target: { host: { ... } },那host设置会被忽略。因为arch已经限定了arm架构,target再写host就没意义了。
  2. sdk_version的默认值:如果不写sdk_version,默认使用当前产品的PLATFORM_SDK_VERSION。这个值在不同产品上可能不同,所以最好显式指定。
  3. product_variables的变量名:必须是全大写,且只能包含字母、数字和下划线。我见过有人用MY-FEATURE(带连字符),结果编译时直接报错。
  4. 条件编译的嵌套:虽然支持嵌套,但我不建议超过两层。比如arch: { arm: { sdk_version: "31" } } 还可以接受,但再往里嵌套product_variables就太复杂了。

重要提醒:条件编译不是万能的。如果某个模块在不同平台上的差异太大,我建议你直接写多个Android.bp文件,或者用不同的模块名。比如my_lib_arm和my_lib_x86,而不是在一个模块里写满条件编译。这样更清晰,也更容易维护。

好了,条件编译的内容就讲到这里。记住一句话:条件编译是为了简化,不是为了复杂。能用简单方式解决的问题,就不要用复杂的条件编译。


公众号:蓝海资料掘金营,微信deep3321