第二章:源码结构解析与编译系统
说实话,我刚接触Android源码那会儿,第一反应就是——这目录也太多了吧?
AOSP源码解压出来,少说几十个G。你打开一看,几百个文件夹堆在那儿。很多人第一步就懵了。别急,我带你捋一遍。
2.1 Android源码目录结构
我们先看顶层。A133的Android源码,顶层目录大概长这样:
.
├── art # Android Runtime
├── bionic # C库
├── bootable # 启动引导
├── build # 编译系统核心
├── cts # 兼容性测试
├── dalvik # Dalvik虚拟机(老版本)
├── developers # 开发者文档
├── device # 设备相关配置
├── external # 第三方库
├── frameworks # 核心框架
├── hardware # 硬件抽象层
├── kernel # Linux内核
├── libcore # Java核心库
├── libnativehelper # Native辅助库
├── ndk # NDK
├── out # 编译输出
├── packages # 应用包
├── pdk # 平台开发套件
├── prebuilts # 预编译工具
├── sdk # SDK
├── system # 系统组件
├── tools # 开发工具
└── vendor # 厂商定制
嗯,这里要注意。你真正需要关心的,其实没那么多。
我个人习惯,把目录分成三类:
- 核心层:frameworks、system、hardware、kernel——这些是系统骨架
- 定制层:device、vendor、packages——这些是我们改得最多的
- 工具层:build、prebuilts、out——编译相关的
我在项目中遇到过很多次,新人一上来就钻到frameworks里改代码,结果发现编译不过。为什么?因为device目录下的BoardConfig.mk没配好。所以我的建议是:先从device和vendor入手,理解硬件配置,再往上走。
2.2 编译系统:从Makefile到Soong
Android的编译系统,经历过一次大变革。
早期版本(Android 7.0之前)用的是纯Makefile。那时候的build目录下,全是.mk文件。你想想看,一个大型项目用Makefile来管理,依赖关系复杂到让人崩溃。我记得有一次,改了一个底层库,结果编译了整整两个小时——因为Makefile把整个系统都重新链接了一遍。
从Android 7.0开始,Google引入了Soong构建系统。Soong用Blueprint文件(.bp)替代了大部分Makefile。说白了,就是把配置从Make语法换成了类似JSON的格式,更清晰,也更快。
核心变化:
- Makefile(.mk)→ Blueprint(.bp)
- 全局变量 → 模块化声明
- 隐式依赖 → 显式依赖
举个例子。老版本的Makefile写法:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c
include $(BUILD_EXECUTABLE)
新版本的Blueprint写法:
cc_binary {
name: "hello",
srcs: ["hello.c"],
}
是不是清爽多了?
但注意,不是所有模块都迁移到了Soong。内核、一些底层驱动、还有vendor目录下的部分代码,仍然在用Makefile。所以你现在看到的编译系统,其实是两者混用的。
避坑指南:我曾经在移植一个外设驱动时,把Makefile改成了Blueprint,结果编译通过了,但运行时一直崩溃。查了两天才发现,是Blueprint的依赖声明和Makefile的隐式依赖不一致。所以,不要轻易把Makefile改成Blueprint,除非你完全理解了依赖关系。
2.3 编译命令详解
好,到了实操环节。编译Android系统,你绕不开这几个命令。
2.3.1 lunch:选择编译目标
每次打开终端,第一件事就是:
source build/envsetup.sh
lunch
lunch命令会列出所有可用的编译目标。比如:
1. aosp_arm-eng
2. aosp_arm64-eng
3. aosp_x86-eng
4. aosp_x86_64-eng
5. aosp_car_arm-userdebug
...
你可能会问,这些名字是什么意思?
格式是:设备名-编译类型
编译类型有三种:
| 类型 | 说明 | 适用场景 |
|---|---|---|
| eng | 工程版,带调试工具 | 开发调试 |
| userdebug | 用户调试版,有root权限 | 测试验证 |
| user | 用户版,无root权限 | 正式发布 |
我个人习惯,开发阶段用eng,测试阶段用userdebug,发布前才切到user。为什么?因为eng版本会编译很多调试工具,比如adb、logcat这些,方便排查问题。但eng版本性能差一些,不适合做性能测试。
2.3.2 make:全量编译
选好目标后,直接:
make -j8
这个命令会编译整个Android系统。第一次编译,A133平台大概需要1-2小时。如果你的机器是8核16线程,用-j16会快一些。
但注意,全量编译很费时间。我一般只在以下情况才用:
- 第一次编译
- 修改了底层库(如bionic、art)
- 修改了编译系统本身
2.3.3 m、mm、mmm:增量编译
日常开发中,你改的只是某个模块。这时候用全量编译就是浪费时间。Soong提供了几个增量编译命令:
| 命令 | 作用 | 示例 |
|---|---|---|
| m | 编译当前目录下的模块 | m |
| mm | 编译当前目录及其子目录 | mm |
| mmm | 编译指定目录 | mmm packages/apps/Settings |
举个例子。你改了Settings应用的代码,只需要:
mmm packages/apps/Settings
然后:
make snod
这个make snod是快速打包system镜像的命令。它不会重新编译,只是把编译好的文件打包成镜像。整个过程,快的话30秒就搞定了。
小技巧:我经常用mmm配合make snod来做快速验证。改完代码,跑一下这两个命令,然后刷机看效果。比全量编译快太多了。
2.3.4 其他常用命令
还有一些命令,虽然不常用,但关键时刻能救命:
make clean:清理所有编译产物。如果你遇到奇怪的编译错误,先跑这个试试。make clobber:比clean更彻底,连中间文件都删了。make update-api:当你修改了framework的API时,需要跑这个来更新API文档。make -j8 2>&1 | tee build.log:把编译日志保存到文件,方便排查错误。
避坑指南:我曾经有一次,改了一个头文件,然后直接mmm编译。结果编译通过了,但运行时一直崩溃。查了半天才发现,是头文件改了,但依赖这个头文件的其他模块没有重新编译。所以,如果你改了公共头文件,最好用make全量编译,或者至少把依赖的模块都编译一遍。
2.4 实战:编译一个模块并刷机
说了这么多,我们来走一遍完整的流程。
假设你改了Settings应用,想看看效果:
- 打开终端,进入源码根目录
source build/envsetup.shlunch,选择你的目标(比如a133_xxx-eng)mmm packages/apps/Settingsmake snod- 刷机:
fastboot flash system out/target/product/a133/system.img - 重启:
fastboot reboot
整个过程,熟练的话5分钟搞定。
嗯,这就是我日常的工作流。你想想看,如果每次改个UI都要全量编译,那效率得多低?
2.5 小结
这一章我们聊了:
- Android源码的目录结构,哪些目录是核心,哪些是定制层
- 从Makefile到Soong的演进,以及两者混用的现状
- lunch、make、m、mm、mmm等编译命令的使用场景
- 一个完整的编译-刷机流程
下一章,我们会深入device目录,看看怎么为A133平台配置硬件参数。到时候,你会真正理解BoardConfig.mk和Device.mk里那些参数的含义。
公众号:蓝海资料掘金营,微信deep3321