多目标编译:同时编译多个产品、多架构支持、分平台编译

说实话,多目标编译这个话题,是很多Android工程师从「能用」到「会用」的分水岭。我刚入行那会儿,觉得能跑通make就挺厉害了。直到有一次,我需要同时给三个不同厂商的设备编译系统镜像……嗯,那场面,至今难忘。

今天我们就来聊聊,Android Build系统里,怎么做到「一套代码,多路输出」。

为什么需要多目标编译?

你想想看,一个手机厂商,可能有高端机、中端机、平板、甚至电视盒子。这些设备用的芯片不同、屏幕不同、硬件配置也不同。如果每个设备都维护一套独立的代码仓库,那维护成本就爆炸了。

多目标编译的核心思想就是:代码共享,配置分离。同一个AOSP源码树,通过不同的编译配置,产出不同的镜像文件。

核心概念:在Android Build系统中,一个「产品」就是一个编译目标。每个产品有自己的BoardConfig.mkdevice.mkproduct.mk等配置文件。

多产品编译:Lunch 的秘密

用过AOSP的人都知道lunch命令。但你真的理解它在做什么吗?

我个人习惯把lunch理解为「切换编译上下文」。它做了三件事:

  1. 选择产品名称(比如aosp_arm64
  2. 选择编译变体(user / userdebug / eng)
  3. 加载对应的BoardConfig和ProductConfig

举个例子:

# 编译Pixel 6的user版本
lunch aosp_bluejay-user

# 编译通用arm64的debug版本
lunch aosp_arm64-eng

# 编译模拟器版本
lunch sdk_phone_x86_64-userdebug

每次执行lunch,系统就会设置TARGET_PRODUCTTARGET_BUILD_VARIANTTARGET_ARCH等环境变量。后面的make命令就根据这些变量来决定编译什么。

小技巧:如果你经常切换产品,可以写个shell脚本,把常用的lunch命令存起来。我一般会在项目根目录放一个lunch_aliases.sh,省得每次敲一长串。

多架构支持:32位 vs 64位

Android设备现在基本都是64位CPU了,但很多App还是32位的。这就引出一个问题:系统镜像里,到底要放哪些库?

Android Build系统通过TARGET_ARCHTARGET_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.bpAndroid.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

实战:如何添加一个新产品的编译目标?

假设我们要给一个「蓝海平板」添加编译支持。步骤其实不复杂:

  1. device/下创建厂商目录:device/lanhai/lanhai_tablet/
  2. 创建BoardConfig.mk,定义硬件参数
  3. 创建device.mk,声明设备模块
  4. 创建product.mk,定义产品名称和属性
  5. 创建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流程图

下面这张图,展示了多目标编译的核心流程:

多目标编译核心流程 AOSP 源码树 lunch 选择产品 + 变体 aosp_arm64-user / pixel6-eng / ... 加载对应产品的配置文件 BoardConfig.mk → 硬件参数 | device.mk → 模块列表 | product.mk → 产品属性 产品A:高通平台 产品B:联发科平台 产品C:展讯平台 out/target/product/产品A/ out/target/product/产品B/ out/target/product/产品C/

避坑指南

多目标编译看着简单,实际用起来坑不少。我把自己踩过的坑分享给你:

  • 缓存污染:切换产品后,一定要执行make cleanmake clobber。我之前偷懒没清理,结果编译出来的镜像里混了上一个产品的驱动,刷到设备上直接黑屏。
  • 模块冲突:不同产品可能依赖同一个模块的不同版本。建议用PRODUCT_PACKAGES精确控制每个产品包含的模块,不要用全局变量。
  • 架构混用:如果产品同时支持32位和64位,记得检查所有预编译库是否都提供了两个架构的版本。少一个,运行时就会报dlopen failed

我的习惯:每次新建产品时,我会先复制一个已有的、相似的产品配置,然后逐步修改。这样不容易漏掉关键配置。改完之后,先编译一个eng版本验证,没问题再切到user版本。

小结

多目标编译,说白了就是「一次配置,到处编译」。Android Build系统通过产品、架构、平台三个维度,把复杂的硬件差异抽象成了配置文件。你只需要理解lunchBoardConfigproduct.mk这几个核心概念,就能轻松驾驭多目标编译。

嗯,下次当你需要同时维护五六个产品时,记得回来翻翻这篇文章。相信我,你会感谢自己的。


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