
1. 项目概述为什么我们需要在Android.mk里“做判断”如果你在Android源码树下做过开发或者维护过需要编译进Android系统的模块那你对Android.mk文件一定不陌生。这个文件就像是每个模块的“编译说明书”告诉构建系统我这个模块叫什么名字需要哪些源文件依赖哪些库最后要生成什么。但很多时候事情没那么简单。比如你的模块可能需要在不同的Android版本API Level下编译不同的代码或者你的模块需要根据当前编译的目标架构armeabi-v7a, arm64-v8a, x86等来链接不同的预编译库又或者你的功能在“用户版本”user和“调试版本”userdebug/eng下需要有差异比如在调试版本中打开更多的日志和调试接口。这时候如果只有一个静态的、一成不变的Android.mk你就得为每一种情况写一个单独的文件或者用一堆注释来手动切换配置这无疑是低效且容易出错的。Android.mk的判断语句就是为解决这类“条件编译”问题而生的。它允许你在构建脚本中根据不同的条件动态地决定包含哪些源文件、定义哪些宏、链接哪些库甚至决定整个模块是否参与编译。这就像给你的构建脚本装上了“大脑”让它能根据环境自动做出决策。理解并熟练运用这些判断语句是从“会写”Android.mk到“写好”Android.mk的关键一步。它能让你编写的模块更具适应性、更健壮也能让你在应对复杂的、多条件的构建需求时游刃有余。今天我们就来深入拆解Android.mk中这些条件判断的语法、常见应用场景以及那些官方文档里可能没细说的“坑”。2. Android.mk判断语句核心语法全解析Android.mk的语法本质上是GNU Makefile语法的一个子集和扩展。因此它的条件判断语句也继承自Makefile。最核心、最常用的判断语句是ifeq、ifneq、ifdef和ifndef。2.1 相等与不等判断ifeq/ifneq这是最直观的判断用于比较两个字符串是否相等。语法格式ifeq (arg1, arg2) # 如果 arg1 等于 arg2则执行这里的语句 else # 否则执行这里的语句else分支可选 endififneq的用法完全相同只是逻辑相反当arg1不等于arg2时执行其分支。关键点解析逗号与空格ifeq后面的括号内参数用逗号分隔。逗号前后可以有空格但为了清晰通常建议写为ifeq (arg1, arg2)。参数引用arg1和arg2可以是直接的字符串也可以是Makefile变量。如果是变量需要用$()或${}包裹起来进行取值。字符串精确匹配比较是大小写敏感的精确字符串匹配。foo和Foo是不相等的。实战示例根据产品型号选择配置假设我们有一个硬件抽象层HAL模块需要为product_a和product_b两款设备编译不同的源文件。# 假设在构建时通过环境变量或命令行传递了 TARGET_PRODUCT LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : my_hal_module # 判断目标产品 ifeq ($(TARGET_PRODUCT), product_a) LOCAL_SRC_FILES : hal_for_product_a.c LOCAL_CFLAGS -DFEATURE_A_ENABLED else ifeq ($(TARGET_PRODUCT), product_b) LOCAL_SRC_FILES : hal_for_product_b.c LOCAL_CFLAGS -DFEATURE_B_ENABLED else $(error Unsupported product: $(TARGET_PRODUCT)) endif LOCAL_SHARED_LIBRARIES : liblog libcutils include $(BUILD_SHARED_LIBRARY)在这个例子中我们根据TARGET_PRODUCT变量的值动态地设置了LOCAL_SRC_FILES和LOCAL_CFLAGS。如果遇到不支持的型号我们使用$(error ...)主动报错停止构建这是一种良好的防御性编程实践。2.2 定义判断ifdef/ifndef这两个语句用于判断一个Makefile变量是否已经被定义了即是否被赋予了一个非空的值。这在检查某个功能开关或配置是否被设置时非常有用。语法格式ifdef VARIABLE_NAME # 如果 VARIABLE_NAME 被定义了非空则执行这里的语句 endififndef则相反当变量未定义或为空时执行。关键点解析“定义”的含义在Makefile中一个变量即使被定义为空VARIABLE :它也算是被“定义”了。ifdef判断的是变量名是否存在以及其值是否非空。一个从未出现过的变量名或者其值为空对于ifdef来说都是“假”。与ifeq的区别ifdef只关心变量有没有值不关心值是什么。而ifeq ($(VAR), value)则既检查VAR有值还检查其值是否等于value。实战示例调试版本与发布版本差异化Android编译系统通常会设置TARGET_BUILD_VARIANT变量其值可能是user、userdebug或eng。我们可以利用ifdef或ifeq来为调试版本增加功能。LOCAL_CFLAGS : -DNDEBUG -O2 # 默认的发布版本配置 # 方法一使用 ifneq 检查是否为 user 版本 ifneq ($(TARGET_BUILD_VARIANT),user) # 如果不是user版本即userdebug或eng添加调试信息 LOCAL_CFLAGS -DDEBUG -g -O0 LOCAL_CFLAGS -DLOG_VERBOSE endif # 方法二使用 ifdef 检查某个自定义调试开关是否被打开 # 假设我们允许通过命令行使能详细日志make MY_MODULE_VERBOSE1 ifdef MY_MODULE_VERBOSE LOCAL_CFLAGS -DMY_MODULE_VERBOSE_LOGGING1 endif注意ifdef对于检查那些可能通过环境变量或命令行传入的、简单的“开关型”变量特别方便。你不需要知道它的值具体是1还是true只要它被设置了非空条件就成立。2.3 逻辑组合与复杂条件判断简单的ifeq和ifdef可以解决大部分问题但遇到复杂逻辑时我们需要将它们组合起来。1. 嵌套判断这是最直接的方式将一个判断语句放在另一个判断语句的分支里。ifeq ($(TARGET_ARCH),arm) # 如果是ARM架构 ifeq ($(TARGET_ARCH_VARIANT),armv7-a-neon) LOCAL_CFLAGS -DUSE_ARM_NEON LOCAL_SRC_FILES neon_optimized.c endif else ifeq ($(TARGET_ARCH),x86) # 如果是x86架构 LOCAL_CFLAGS -DUSE_SSE2 endif2. 使用逻辑函数组合Android.mk实际上是Makefile提供了and、or、not等逻辑函数但它们的语法比较晦涩。更常见的做法是利用Makefile的语法特性来实现“与”和“或”。“与”逻辑AND通过嵌套ifeq实现。要求所有条件都满足。# 判断既是userdebug版本又是64位架构 ifeq ($(TARGET_BUILD_VARIANT),userdebug) ifeq ($(TARGET_IS_64_BIT),true) # 两个条件都满足执行这里 LOCAL_CFLAGS -DDEBUG_64BIT endif endif也可以写在一行但可读性较差ifeq ($(TARGET_BUILD_VARIANT)-$(TARGET_IS_64_BIT),userdebug-true)“或”逻辑OR通过多个ifeq分支实现或者使用filter函数。要求任一条件满足。# 方法A使用多个else if ifeq ($(TARGET_ARCH),arm) # ARM架构 else ifeq ($(TARGET_ARCH),aarch64) # ARM64架构执行与ARM相同的逻辑 endif # 注意这种方法下两个分支的代码如果相同就需要重复写。 # 方法B使用filter函数更优雅 # filter函数用于检查一个字符串是否匹配给定模式列表中的任何一个 ifneq ($(filter arm aarch64,$(TARGET_ARCH)),) # 如果TARGET_ARCH的值是arm或aarch64中的一个filter结果非空 LOCAL_CFLAGS -DARM_FAMILY LOCAL_SRC_FILES arm_common.c endiffilter函数在这里非常实用。$(filter arm aarch64,$(TARGET_ARCH))的意思是从列表arm aarch64中过滤出与$(TARGET_ARCH)相等的项。如果过滤后结果非空即匹配上了则条件成立。3. 实战应用判断语句的典型使用场景掌握了语法我们来看看在真实的Android模块开发中这些判断语句都用在哪些刀刃上。3.1 场景一多架构适配Prebuilt库与源码选择这是最经典的应用。Android设备有armeabi-v7a、arm64-v8a、x86、x86_64等多种CPU架构。你的模块可能需要为不同架构链接不同的预编译库.so或.a。LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : my_native_lib LOCAL_SRC_FILES : common_logic.cpp # 根据目标架构链接不同的预编译库 ifeq ($(TARGET_ARCH),arm) # 32位ARM LOCAL_SRC_FILES arm/arm_specific.cpp LOCAL_SHARED_LIBRARIES prebuilt_arm_lib else ifeq ($(TARGET_ARCH),aarch64) # 64位ARM LOCAL_SRC_FILES arm64/arm64_specific.cpp LOCAL_SHARED_LIBRARIES prebuilt_arm64_lib else ifeq ($(TARGET_ARCH),x86) # 32位x86 LOCAL_SRC_FILES x86/x86_specific.cpp LOCAL_SHARED_LIBRARIES prebuilt_x86_lib else ifeq ($(TARGET_ARCH),x86_64) # 64位x86 LOCAL_SRC_FILES x86_64/x86_64_specific.cpp LOCAL_SHARED_LIBRARIES prebuilt_x86_64_lib else $(warning Unsupported architecture: $(TARGET_ARCH). Building without arch-specific optimizations.) endif include $(BUILD_SHARED_LIBRARY) # 预编译库模块的定义也需要条件判断 include $(CLEAR_VARS) LOCAL_MODULE : prebuilt_arm_lib ifeq ($(TARGET_ARCH),arm) LOCAL_SRC_FILES : prebuilts/lib/armeabi-v7a/libfoo.so LOCAL_MODULE_CLASS : SHARED_LIBRARIES LOCAL_MODULE_SUFFIX : .so include $(PREBUILT_SHARED_LIBRARY) endif # ... 为其他架构定义类似的预编译库模块实操心得对于预编译库一个更常见的做法是不在Android.mk里用复杂的ifeq为每个架构写一遍而是利用Android构建系统的模块命名规则。你可以将不同架构的.so文件放在对应的子目录如lib/armeabi-v7a/,lib/arm64-v8a/下然后在Android.mk中只需指定模块名和顶级目录系统会自动根据当前编译的架构选取正确的文件。但当你需要为不同架构编译不同的源码文件时ifeq判断就无可替代了。3.2 场景二Android版本API Level兼容Android不同版本SDK级别会引入或废弃API。你的模块可能需要向后兼容。# 获取当前编译的SDK版本号 TARGET_SDK_VERSION : $(call get-target-sdk-version) LOCAL_CFLAGS : # 判断SDK版本使用不同的编译标志或源文件 ifneq ($(call math_lt, $(TARGET_SDK_VERSION), 23),) # 如果版本小于23 (Android M) # 对于旧版本我们需要使用旧的权限模型或兼容API LOCAL_CFLAGS -DUSE_OLD_PERMISSION_MODEL LOCAL_SRC_FILES compat/legacy_permission_shim.c else # Android M (API 23) 及以上使用新的运行时权限 LOCAL_CFLAGS -DUSE_RUNTIME_PERMISSIONS endif # 另一个例子处理API废弃 ifeq ($(TARGET_SDK_VERSION),28) # Android P LOCAL_CFLAGS -D_HAS_DEPRECATED_API_P endif这里用到了一个辅助函数$(call math_lt, A, B)它并不是标准函数需要你自己定义或使用构建系统提供的。更常见的做法是直接使用ifeq配合filter来检查版本范围或者依赖build/core/version_defaults.mk中定义的变量如PLATFORM_SDK_VERSION。更可靠的版本判断方法# 引入版本定义 include $(BUILD_SYSTEM)/version_defaults.mk # 使用 PLATFORM_SDK_VERSION ifeq ($(call math_gt, $(PLATFORM_SDK_VERSION), 30),true) # 如果大于 Android R (30) LOCAL_CFLAGS -DTARGETS_R_AND_ABOVE endif # 或者使用 codename 判断适用于预览版 ifneq ($(filter S Sv2,$(PLATFORM_VERSION_CODENAME)),) LOCAL_CFLAGS -DPLATFORM_S endif3.3 场景三调试与发布构建差异化我们之前简单提到过这里展开更详细的实践。除了TARGET_BUILD_VARIANT还有很多其他开关。LOCAL_CFLAGS : -Wall -Werror # 1. 基于构建类型的差异化 ifneq ($(TARGET_BUILD_VARIANT),user) # 非user版本eng, userdebug启用调试和日志 LOCAL_CFLAGS -DDEBUG -UNDEBUG -g -O0 LOCAL_CFLAGS -DLOG_NDEBUG0 # 启用LOGV等详细日志 # 包含额外的调试工具源文件 LOCAL_SRC_FILES debug_utils.cpp else # user版本追求尺寸和性能 LOCAL_CFLAGS -DNDEBUG -Os -flto LOCAL_CFLAGS -DLOG_NDEBUG1 # 禁用详细日志 endif # 2. 检查是否启用了AddressSanitizer等检测工具 ifdef $(SANITIZE_TARGET) # 构建系统启用了Sanitizer我们需要调整一些编译选项 LOCAL_CFLAGS -fno-omit-frame-pointer # 可能排除某些不兼容Sanitizer的源文件 # LOCAL_SRC_FILES : $(filter-out problematic_file.c,$(LOCAL_SRC_FILES)) endif # 3. 自定义的详细日志开关通过环境变量控制 MY_DEBUG_ENABLED ? 0 # 默认关闭 ifeq ($(MY_DEBUG_ENABLED),1) LOCAL_CFLAGS -DMY_SUPER_VERBOSE_DEBUG1 $(warning My module debug is enabled!) endif注意事项在user版本中关闭调试日志通过-DLOG_NDEBUG1是非常重要的。这不仅能减少二进制大小还能避免在用户设备上泄露敏感调试信息。-UNDEBUG用于取消可能在其他地方定义的NDEBUG宏确保在调试版本中assert()函数能正常工作。4. 进阶技巧与避坑指南写了几年的Android.mk我踩过的坑比顺利编译的次数还多。下面这些经验希望能帮你绕开那些恼人的问题。4.1 变量求值时机与空格陷阱这是Makefile新手和老手都容易栽跟头的地方。问题Makefile的变量展开和条件判断发生在解析阶段而不是执行阶段。这意味着如果你在条件分支内部定义一个变量然后试图在同一个条件块里使用它来判断另一个条件是行不通的。错误示例ifeq ($(TARGET_ARCH),arm) MY_SUB_ARCH : armv7-a endif # 这里试图根据上面设置的 MY_SUB_ARCH 做进一步判断但可能失败 ifeq ($(MY_SUB_ARCH),armv7-a) # MY_SUB_ARCH 可能在这里还未被定义或展开 LOCAL_CFLAGS -marcharmv7-a endif在第一个ifeq解析时MY_SUB_ARCH被赋值为armv7-a。但当解析到第二个ifeq时Makefile会立即对条件$(MY_SUB_ARCH),armv7-a进行求值。然而由于Makefile的解析规则这种跨条件块的变量依赖可能因为作用域问题导致$(MY_SUB_ARCH)为空从而使判断失败。解决方案嵌套判断将依赖的判断直接嵌套进去。ifeq ($(TARGET_ARCH),arm) MY_SUB_ARCH : armv7-a # 直接在这里写后续逻辑 LOCAL_CFLAGS -marcharmv7-a endif使用中间变量和strip函数如果逻辑复杂必须分开确保使用strip函数处理变量并理解变量的全局/局部作用域。MY_SUB_ARCH : ifeq ($(TARGET_ARCH),arm) MY_SUB_ARCH : armv7-a endif # 使用 strip 去除可能的空格使比较更安全 ifeq ($(strip $(MY_SUB_ARCH)),armv7-a) LOCAL_CFLAGS -marcharmv7-a endif空格陷阱ifeq ($(TARGET_ARCH), arm) # 注意第二个参数前有个空格 # 这永远不会成立因为比较的是 arm 和 arm endif最佳实践在ifeq的参数中避免在逗号后和括号内引入不必要的空格。写成ifeq ($(TARGET_ARCH),arm)最安全。4.2 使用filter和findstring进行模式匹配当你的条件不是简单的“等于”而是“属于某一个集合”时filter和findstring是你的好帮手。filter如前所述检查是否匹配列表中的任一项。# 判断是否是64位架构 ifneq ($(filter arm64 x86_64,$(TARGET_ARCH)),) LOCAL_CFLAGS -DARCH_64BIT endiffindstring在一个字符串中查找子串。# 判断编译器版本是否包含特定字符串例如检查是否是Clang ifneq ($(findstring clang,$(NDK_TOOLCHAIN_VERSION)),) LOCAL_CFLAGS -DUSING_CLANG endif # 判断版本号中是否包含“预览版”标识 ifneq ($(findstring PREVIEW,$(PLATFORM_VERSION)),) LOCAL_CFLAGS -DPREVIEW_SDK endififneq ($(findstring..., ...),)这个组合的意思是如果找到了子串findstring返回非空字符串则条件成立。4.3 优雅处理“未定义”与默认值不要总是用ifdef去检查然后else给个默认值。Makefile提供了更简洁的语法?和:。?条件赋值仅当变量之前没有被赋值时才进行赋值。# 如果MY_FEATURE_FLAG没有被命令行或环境定义则给它默认值1 MY_FEATURE_FLAG ? 1这完全等价于ifndef MY_FEATURE_FLAG MY_FEATURE_FLAG 1 endif但显然?更简洁。:立即展开赋值 vs递归展开赋值在条件判断相关的变量赋值中我强烈推荐使用:。# 使用 :变量的值在定义时立即确定 CURRENT_ARCH : $(TARGET_ARCH) ifeq ($(CURRENT_ARCH),arm) # 这里总是能正确判断 ... endif # 使用 变量的值会在每次引用时才展开在复杂脚本中可能导致意想不到的结果 # DELAYED_ARCH $(TARGET_ARCH) # 可能导致问题在Android.mk这种相对简单的脚本中为了可预测性对所有自定义变量使用:是个好习惯。4.4 调试你的条件判断$(warning)和$(error)当你的条件判断没有按预期工作时别光靠猜。把关键变量的值打出来看看。# 在判断前打印变量值 $(warning TARGET_ARCH is set to: $(TARGET_ARCH)) $(warning TARGET_BUILD_VARIANT is: $(TARGET_BUILD_VARIANT)) ifeq ($(TARGET_ARCH),arm) $(info Building for ARM architecture...) # $(info)输出更安静一些 ... endif # 如果遇到不应该发生的情况用$(error)果断报错停止构建 ifndef MY_REQUIRED_VAR $(error MY_REQUIRED_VAR is not defined. Please set it in your environment.) endif$(warning ...)会输出一条警告信息但构建继续。$(error ...)会输出错误信息并立即停止构建。$(info ...)则输出普通信息。这是调试Android.mk逻辑最直接有效的方法。5. 从Android.mk到Android.bp条件编译的变迁虽然本文重点在Android.mk但必须提一下Android构建系统的未来——Soong和Android.bp。Android.bp是Google引入的新的模块定义文件旨在替代Android.mk。它的语法更简洁不依赖于Makefile。那么在Android.bp中如何做条件判断呢答案是原则上不鼓励在模块定义内部做条件判断。Soong的设计哲学是将条件逻辑转移到更上层的产品配置和变异体中。实现方式target:和host:最基本的条件用于区分在设备上运行的目标模块和在编译机上运行的宿主模块。cc_binary { name: my_tool, target: { android: { srcs: [android_specific.c], }, host: { srcs: [host_specific.c], }, }, }产品变量与变异体复杂的条件如不同产品、不同架构、调试/发布通过Go语言编写的蓝图Blueprint文件来处理。这些文件通常位于build/soong/目录下可以定义全局变量、设置默认值并基于这些变量为模块设置不同的属性。条件属性Conditional Properties在Android.bp中你可以使用类似字典的语法来根据某些条件设置属性但这通常依赖于Soong插件提供的支持不是通用语法。迁移建议如果你正在将Android.mk迁移到Android.bp遇到复杂的ifeq判断时你需要思考这个判断是基于什么维度架构产品构建类型这个维度的不同选择是否应该被视为不同的模块变体如果是那么更合适的做法可能是定义多个cc_library或cc_binary模块让构建系统根据配置自动选择正确的模块而不是在一个模块文件里用条件语句写死所有逻辑。核心区别总结Android.mk命令式。你像写程序一样用ifeq、ifdef等语句告诉构建系统“如果……就……”。Android.bp/ Soong声明式。你声明模块的属性和可能的变体构建系统根据全局配置由Go代码定义来决定如何实例化这个模块。对于维护现有Android.mk的开发者来说深入理解其中的判断语句依然是必备技能。而对于新项目如果可能直接学习并使用Android.bp是更面向未来的选择。理解Android.mk的条件逻辑也能帮助你更好地设计Android.bp的模块结构因为背后的需求条件编译是相通的只是实现的范式发生了变化。