多目标编译:同时编译多个产品、多架构支持、分平台编译
说实话,多目标编译这个话题,是很多Android工程师从「能用」到「会用」的分水岭。我刚入行那会儿,觉得能跑通make就挺厉害了。直到有一次,我需要同时给三个不同厂商的设备编译系统镜像……嗯,那场面,至今难忘。
今天我们就来聊聊,Android Build系统里,怎么做到「一套代码,多路输出」。
为什么需要多目标编译?
你想想看,一个手机厂商,可能有高端机、中端机、平板、甚至电视盒子。这些设备用的芯片不同、屏幕不同、硬件配置也不同。如果每个设备都维护一套独立的代码仓库,那维护成本就爆炸了。
多目标编译的核心思想就是:代码共享,配置分离。同一个AOSP源码树,通过不同的编译配置,产出不同的镜像文件。
核心概念:在Android Build系统中,一个「产品」就是一个编译目标。每个产品有自己的BoardConfig.mk、device.mk、product.mk等配置文件。
多产品编译:Lunch 的秘密
用过AOSP的人都知道lunch命令。但你真的理解它在做什么吗?
我个人习惯把lunch理解为「切换编译上下文」。它做了三件事:
- 选择产品名称(比如
aosp_arm64) - 选择编译变体(user / userdebug / eng)
- 加载对应的BoardConfig和ProductConfig
举个例子:
# 编译Pixel 6的user版本
lunch aosp_bluejay-user
# 编译通用arm64的debug版本
lunch aosp_arm64-eng
# 编译模拟器版本
lunch sdk_phone_x86_64-userdebug
每次执行lunch,系统就会设置TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_ARCH等环境变量。后面的make命令就根据这些变量来决定编译什么。
小技巧:如果你经常切换产品,可以写个shell脚本,把常用的lunch命令存起来。我一般会在项目根目录放一个lunch_aliases.sh,省得每次敲一长串。
多架构支持:32位 vs 64位
Android设备现在基本都是64位CPU了,但很多App还是32位的。这就引出一个问题:系统镜像里,到底要放哪些库?
Android Build系统通过TARGET_ARCH和TARGET_2ND_ARCH来处理多架构。说白了,就是「主架构」和「次架构」。
看一个典型的BoardConfig配置:
# 64位为主,32位为辅
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_2ND_ARCH := arm
TARGET_2ND_ARCH_VARIANT := armv7-a-neon
TARGET_2ND_CPU_VARIANT := generic
这样配置后,系统会同时编译arm64和arm两个版本的库。App运行时,64位的进程加载/system/lib64下的库,32位的进程加载/system/lib下的库。
我曾经踩过一个坑:某个硬件抽象层(HAL)模块只编译了64位版本,结果32位的App调用摄像头时直接崩溃。排查了半天才发现是库没编译32位版本。
注意:不是所有模块都需要编译两个架构。如果某个模块只在64位进程中使用,可以只编译64位版本。在Android.bp或Android.mk中通过target字段控制。
分平台编译:不同芯片,不同配置
多平台支持,说白了就是「同一套代码,适配不同的硬件平台」。比如高通平台、联发科平台、展讯平台,它们的BSP(板级支持包)完全不同。
Android Build系统通过device/目录来组织不同平台的代码。典型的目录结构是这样的:
device/
├── google/
│ ├── pixel6/
│ └── pixel7/
├── qcom/
│ ├── sm8250/
│ └── sm8450/
└── mediatek/
└── mt6893/
每个平台目录下,都有自己的一套配置文件:
BoardConfig.mk:定义硬件相关的参数(内核、分区大小、GPU等)device.mk:定义设备相关的模块(驱动、HAL、固件等)product.mk:定义产品相关的配置(预装App、系统属性等)
编译时,通过lunch选择不同的产品,系统就会自动加载对应平台的配置。比如:
# 编译高通平台的产品
lunch qcom_sm8450-userdebug
# 编译联发科平台的产品
lunch mt6893_64-userdebug
实战:如何添加一个新产品的编译目标?
假设我们要给一个「蓝海平板」添加编译支持。步骤其实不复杂:
- 在
device/下创建厂商目录:device/lanhai/lanhai_tablet/ - 创建
BoardConfig.mk,定义硬件参数 - 创建
device.mk,声明设备模块 - 创建
product.mk,定义产品名称和属性 - 创建
AndroidProducts.mk,注册产品
一个最简单的AndroidProducts.mk示例:
PRODUCT_MAKEFILES := \
$(LOCAL_DIR)/lanhai_tablet.mk
然后lanhai_tablet.mk里引用其他配置文件:
$(call inherit-product, device/lanhai/lanhai_tablet/device.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
PRODUCT_NAME := lanhai_tablet
PRODUCT_DEVICE := lanhai_tablet
PRODUCT_BRAND := LanHai
PRODUCT_MODEL := LH-T100
搞定之后,你就可以用lunch lanhai_tablet-userdebug来编译了。
多目标编译的SVG流程图
下面这张图,展示了多目标编译的核心流程:
避坑指南
多目标编译看着简单,实际用起来坑不少。我把自己踩过的坑分享给你:
- 缓存污染:切换产品后,一定要执行
make clean或make clobber。我之前偷懒没清理,结果编译出来的镜像里混了上一个产品的驱动,刷到设备上直接黑屏。 - 模块冲突:不同产品可能依赖同一个模块的不同版本。建议用
PRODUCT_PACKAGES精确控制每个产品包含的模块,不要用全局变量。 - 架构混用:如果产品同时支持32位和64位,记得检查所有预编译库是否都提供了两个架构的版本。少一个,运行时就会报
dlopen failed。
我的习惯:每次新建产品时,我会先复制一个已有的、相似的产品配置,然后逐步修改。这样不容易漏掉关键配置。改完之后,先编译一个eng版本验证,没问题再切到user版本。
小结
多目标编译,说白了就是「一次配置,到处编译」。Android Build系统通过产品、架构、平台三个维度,把复杂的硬件差异抽象成了配置文件。你只需要理解lunch、BoardConfig、product.mk这几个核心概念,就能轻松驾驭多目标编译。
嗯,下次当你需要同时维护五六个产品时,记得回来翻翻这篇文章。相信我,你会感谢自己的。
公众号:蓝海资料掘金营,微信deep3321