第二章:源码结构解析与编译系统

说实话,我刚接触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应用,想看看效果:

  1. 打开终端,进入源码根目录
  2. source build/envsetup.sh
  3. lunch,选择你的目标(比如a133_xxx-eng)
  4. mmm packages/apps/Settings
  5. make snod
  6. 刷机:fastboot flash system out/target/product/a133/system.img
  7. 重启:fastboot reboot

整个过程,熟练的话5分钟搞定。

嗯,这就是我日常的工作流。你想想看,如果每次改个UI都要全量编译,那效率得多低?

2.5 小结

这一章我们聊了:

  • Android源码的目录结构,哪些目录是核心,哪些是定制层
  • 从Makefile到Soong的演进,以及两者混用的现状
  • lunch、make、m、mm、mmm等编译命令的使用场景
  • 一个完整的编译-刷机流程

下一章,我们会深入device目录,看看怎么为A133平台配置硬件参数。到时候,你会真正理解BoardConfig.mk和Device.mk里那些参数的含义。

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