第4章:Android.mk 入门:文件结构、LOCAL_ 变量详解、编译一个简单的 Hello World 模块

好,咱们今天来聊聊 Android.mk。说实话,这是 Android 构建系统里最基础、也最绕不开的一个文件。我刚开始接触 Android 源码时,第一件事就是被前辈扔过来一个 Android.mk,让我自己看。当时我盯着那些 LOCAL_ 开头的变量,心里直犯嘀咕——这玩意儿到底是啥?

别急,咱们一步步拆解。Android.mk 说白了就是一个 Makefile 的变种,专门给 Android 的构建系统用的。它的核心任务就一个:告诉系统「我要编译什么、怎么编译、编译完放哪儿」。

4.1 Android.mk 文件结构

一个标准的 Android.mk 文件,结构其实非常清晰。我习惯把它分成三个部分:

  • 头部声明:定义模块的路径、清除之前的变量
  • 核心配置:用 LOCAL_ 变量描述模块信息
  • 尾部编译:调用编译命令,生成目标

来看一个最简单的例子:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := hello_world
LOCAL_SRC_FILES := hello_world.c

include $(BUILD_EXECUTABLE)

嗯,就这么几行。你想想看,一个可执行文件就定义好了。是不是比想象中简单?

这里有个细节我要提一下:$(call my-dir) 这个函数,它会返回当前 Android.mk 文件所在的目录路径。我在项目中遇到过有人把源文件放错路径,结果编译出来是个空壳子——就是因为 LOCAL_PATH 没写对。

4.2 LOCAL_ 变量详解

LOCAL_ 变量是 Android.mk 的灵魂。我列几个最常用的,你记牢了,后面写模块基本够用:

变量名 作用 常见取值
LOCAL_MODULE 模块名称,唯一标识 hello_world, libfoo, test_app
LOCAL_SRC_FILES 源文件列表 main.c, utils.cpp, foo/bar.c
LOCAL_C_INCLUDES 头文件搜索路径 $(LOCAL_PATH)/include, external/lib
LOCAL_SHARED_LIBRARIES 依赖的共享库 libcutils, liblog, libutils
LOCAL_STATIC_LIBRARIES 依赖的静态库 libfoo_static, libbar
LOCAL_CFLAGS 编译选项 -Wall -O2 -DDEBUG=1
LOCAL_LDLIBS 链接选项 -lz -ldl -lm

这里我要特别说一下 LOCAL_MODULE。这个变量名必须唯一,不能跟系统中已有的模块重名。我曾经有一次把模块名写成了 libc,结果编译直接报错——系统说「你丫想覆盖我的 libc?」。嗯,这种低级错误,犯过一次就记住了。

核心原则:LOCAL_ 变量只在当前 Android.mk 文件中生效。每次 include $(CLEAR_VARS) 之后,所有 LOCAL_ 变量都会被清空。所以每个模块的配置都是独立的,互不干扰。

4.3 编译一个 Hello World 模块

光说不练假把式。咱们来亲手编译一个 Hello World 模块。整个过程分三步:

  1. 写源码:创建一个 hello_world.c 文件
  2. 写 Android.mk:定义模块配置
  3. 执行编译:用 mmm 或 mm 命令编译

先看源码,没什么特别的:

// hello_world.c
#include <stdio.h>

int main() {
    printf("Hello, Android Build System!\n");
    return 0;
}

然后写 Android.mk,放在同一个目录下:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := hello_world
LOCAL_SRC_FILES := hello_world.c
LOCAL_MODULE_TAGS := optional

include $(BUILD_EXECUTABLE)

这里我加了一个 LOCAL_MODULE_TAGS := optional。这个变量表示模块的编译标签,optional 是最常用的——意思是「按需编译」,不会默认打包进系统。如果你想让模块默认编译进系统,可以用 enguser

编译命令很简单。在 Android 源码根目录下执行:

source build/envsetup.sh
lunch aosp_arm-eng
mmm path/to/your/module/

或者如果你已经在模块目录下,直接用 mm 命令:

cd path/to/your/module/
mm

编译成功后,你会看到类似这样的输出:

target Executable: hello_world (out/target/product/generic/obj/EXECUTABLES/hello_world_intermediates/LINKED/hello_world)
Install: out/target/product/generic/system/bin/hello_world

看到 Install 那行了吗?你的 Hello World 已经编译好,并且安装到了 out/target/product/generic/system/bin/hello_world。你可以用 adb push 把它推到设备上运行,或者在模拟器里直接跑。

小技巧:如果你只想编译、不想安装,可以用 mm -B 强制重新编译。我经常在调试时用这个,省得每次都要等安装步骤。

4.4 避坑指南

写 Android.mk 时,有几个坑我踩过,你注意一下:

  • 路径问题LOCAL_SRC_FILES 里的路径是相对于 LOCAL_PATH 的,不是相对于当前目录。我见过有人写 ../src/main.c,结果编译找不到文件。
  • 变量拼写:LOCAL_ 变量名区分大小写。写错了不会报错,但变量不会生效。比如 local_moduleLOCAL_MODULE 是两个东西。
  • 重复定义:同一个 Android.mk 里不要定义两个同名的 LOCAL_MODULE。系统会报错,说模块重复。

警告:不要在 Android.mk 里写复杂的 Makefile 逻辑。Android 构建系统有自己的规则,乱用 ifeqforeach 这些函数,很容易把构建搞崩。我见过一个同事在 Android.mk 里写了 200 行的条件判断,最后编译时间翻了三倍。

4.5 知识体系图

为了让你更直观地理解 Android.mk 的整个流程,我画了一张图:

Android.mk 编译流程 hello_world.c Android.mk 编译引擎 hello_world system/bin/hello_world 源文件 配置文件 解析 + 编译 可执行文件 安装到系统镜像

这张图展示了从源码到最终安装的完整链路。你注意看,Android.mk 在这里扮演的是「翻译官」的角色——它把你想编译的内容,翻译成编译引擎能理解的指令。

4.6 小结

Android.mk 入门其实不难。记住三件事:

  • 结构上分三块:路径声明、变量配置、编译命令
  • LOCAL_ 变量是核心,每个变量都有明确的用途
  • 编译命令用 mmmmm,简单直接

我个人建议,刚开始写 Android.mk 时,先拿一个简单的 Hello World 练手。等你能顺利编译出可执行文件了,再慢慢加其他变量。别一上来就想写复杂的模块——我见过太多人把 Android.mk 写成天书,结果自己都看不懂。

好,这一章就到这儿。下一章咱们聊聊 Android.bp——嗯,那是另一个故事了。


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