读源码这件事,很多人卡在第一步。不是因为源码本身难懂,而是环境、工具、背景知识这些“准备知识”没有打通。深入理解Android第2卷的第1章,标题看起来平平无奇,但它覆盖的恰恰是后续所有源码分析篇目的地基:系统架构如何在宏观层面组织、源码怎么下载对应版本、构建系统为什么这么设计、开发环境又该如何配置。我记得自己第一次接触这套内容时,觉得这一章太“虚”,直接翻到了后面的代码解析,结果没过多久就被绕得头晕,老老实实回来重看这一章。后来带过几个新同事,也几乎都有同样的反复。
这篇文章我想以“准备知识”这一章作为线索,把它拆成四条主线:架构认知、版本与源码、构建系统、环境工具。然后再加上我在实操中踩过、也帮别人排查过的典型问题。定位上,它适合两种人:一种是从应用开发往系统开发走的人,另一种是已经能读代码、但总觉得通读源码没有全局感的人。如果你只是想找一个具体模块的调用链,那可以直接跳过本篇,问题不大。
1. 这一章到底在讲什么:被很多人跳过的“地基”
很多开发者买回这本书,都会下意识翻到第2章、第3章去看Binder怎么运作、Handler怎么处理消息。这个选择不奇怪,毕竟第1章没有贴具体模块的源码,读起来更像一份“说明书”。但我想说,这一章节的价值恰恰藏在“说明书”这三个字里。
1.1 为什么第1章容易被低估
没有源码分析的章节,通常被认为是“软内容”。但从实际收益来算,这一章的性价比极高。它用一个相对集中的篇幅,把Android系统的分层结构、关键代码路径的位置、构建系统的组织方式以及源码编译的基本操作全部串了起来。
举个例子,如果你不知道frameworks/base/core/java/android/os和frameworks/base/services/core/java/com/android/server之间的边界在哪里,第2章分析Binder的时候,你就很难快速区分“客户端代理”和“系统服务实现”分别落在了哪块目录。很多人在看Binder的IBinder接口时觉得绕,其实不是接口本身绕,而是对模块划分没有一个宏观印象,导致每一步都在猜。
我后来在带人做系统开发时,会先让他们把这本书第1章读两遍。不是通读,是带着问题去读:第一遍搞懂“有哪些子系统”,第二遍搞懂“它们怎么串起来”。两遍下来再去看具体模块,进度不是变慢,反而是明显变快。从前端应用开发转过来的人,最容易有的一个误区,是认为系统框架层的代码结构跟应用工程差不多,结果一进frameworks/base看到几千个目录直接懵了。
1.2 章节内容概览与我理解的主线
我个人读完这一章,把内容归纳成几条主线,和目录原顺序不完全一致,按依赖关系重新排列:
- 系统总体架构:应用层、框架层、系统服务、HAL与内核的边界在哪里,谁调用谁。
- 源码工程结构:一个AOSP工程里哪些目录对应框架哪一部分,代码怎么按功能模块和产品定制拆分。
- 版本与分支选择:系统源码和SDK版本如何对应,Tag与分支的区别,不同设备厂商的定制分支怎么形成。
- 构建系统:从早期Make到Soong、Ninja的演进,重点理解模块级构建的配置逻辑。
- 编译与运行环境:源码编译的硬件要求、环境变量、Java版本、磁盘空间等实际硬指标。
- 开发调试工具链:源码阅读索引、日志系统、内核与系统调试的基本手段。
这六条主线如果在一周内消化完,后面的章节就是顺藤摸瓜。如果跳过,那后面基本是查字典式的阅读,累且容易忘。
2. 系统架构与组件划分:骨架先于细节
读系统源码和读普通业务代码有一个本质区别:业务代码是“纵向”的,一条调用路径基本能讲完一个功能;而系统源码是“横向+纵向”交错的,你读Activity启动流程,会发现它同时牵扯窗口管理、进程管理、应用沙箱和渲染等多个子系统。没有骨架,深入细节就会迷失。
2.1 应用框架、系统服务与Native层的关系
Android的系统分层,从外到内大致是:应用层用到的是android.*这组API,这些API的实现多数通过Binder跨进程调用系统服务。系统服务运行在独立的系统进程中,它们负责真正的资源管理、策略判定和状态维护。再往下,系统服务通过JNI调用Native层逻辑,Native层再通过设备驱动触及硬件。
这套结构看起来常规,但它其实解决了一个最难的问题——进程隔离。应用不该直接访问硬件,也不该直接修改系统状态,所以中间必须有一个经过授权的服务层来做“代理”。
这一章明确点出了一个容易混淆的点:应用框架层(app framework)可视作“门面”,它包含的Class定义大多只是接口、Stub和Proxy。真正的实现,在服务端。比如ActivityManager的应用层类(ActivityManager)是一个客户端封装,真正的决策位于ActivityManagerService(通常称为AMS)。这两者的关系如果只看名字,很容易认为ActivityManager就是实现类。
2.2 进程、线程与Binder的全局观
Binder是Android系统里通信的核心,但“准备知识”这一章并不会深挖Binder协议本身,它更侧重让你在宏观上意识到:系统里到处都有Binder,才需要有一套进程间通信协议。
书里有一个类比让我印象很深:把Binder理解成“系统内部的名片”,每个服务都有一张名片(Binder实体),调用方拿到名片(代理),不需要知道名片背后的服务进程具体长什么样,只要递上请求就能得到响应。跨进程调用由此变成一种常态机制,而不是特殊操作。
读完这部分,我建议自己手动做一张进程表,把系统里常见的进程分组梳理出来:
- 系统服务进程,比如
system_server,承载大多数核心服务。 - 应用进程,每个应用一个独立的进程,由Zygote创建。
- Native守护进程,比如
surfaceflinger、media、audioserver,处理图形合成、媒体播放和音频。 - HAL进程或线程,处于Native最底部,与内核驱动交互。
这张表不需要记全,但它能帮你在读第2、3章时快速定位“这个代码运行在哪个进程里”。进程归属一旦搞错,整个调用逻辑的理解就会对不上。
2.3 源码目录结构理解的几个关键坐标
AOSP仓库很大,但目录布局是规律性的。这一章列出的几个关键路径,我个人认为在阅读中要形成肌肉记忆:
frameworks/base:框架层核心,包括Activity、Service、View、ContentProvider四大组件的基础实现,以及系统服务的Java和Native部分。frameworks/native:Native层的基础库,包括Binder、Surface、Input等关键模块的Native实现。system/core:与系统底层相关的库、工具、初始化流程(init)、日志系统等。packages/apps:系统内置应用,比如桌面、设置、通讯录。hardware/libhardware:HAL接口定义,硬件抽象层的标准结构。kernel:内核源码,一般情况下系统开发不必每天看这里,但涉及驱动适配时需进入。
做应用开发的人对packages/apps可能并不陌生,但容易忽略一点:内置应用虽然是普通应用,但它们可以调用一些隐藏API或系统权限接口。因此,单纯把内置应用当作参考代码,是划算但需要甄别的。
3. 源码获取与版本选择:实践者的第一道坎
读书和实战之间最明显的一条沟,就是源码获取。很多人在“准备知识”这里不是不想读,而是卡在了下载和编译阶段。源码工程很大,没有规划就动手,大概率会在一天内放弃。
3.1 源码版本选择与编译环境
源码、SDK、编译工具链、JAVA版本之间存在严格的对应关系。这一点用错了,后面就是无休止的报错。
我的建议是:在动手之前先明确目标,你到底是为了读哪一部分代码。通常分两种场景。
场景一:只想阅读某个模块的代码,比如想看AMS的实现。这种情况下并不需要完整编译整个系统,你可以用源码索引工具或者在线代码浏览器来读,本地只要拉对应的Git仓库即可。
场景二:想修改系统行为、验证定制效果,那才需要完整同步AOSP源码并执行一次完整编译。这种场景下,硬件配置和磁盘空间就是硬指标。AOSP的完整源码同步体积通常超过100GB,再加上编译产物,预留300GB以上空间是比较稳妥的。内存方面,编译framework模块时8GB到16GB能跑,但全量编译建议32GB起步,否则等待时间会非常感人。
版本对应关系上,Android 10(API 29)对应的源码标记是android-10.0.0_rXX;Android 11对应android-11.0.0_rXX,以此类推。读源码时选择一个和你手上设备系统版本接近的Tag,能够避免“源码和设备行为不匹配”的混淆。
3.2 源码拉取策略与分支管理
这一章会教读者用repo工具获取源码。repo init拉取的清单文件决定了你能看到哪些项目,repo sync则是真正拉取代码。对大仓库而言,一次性全量同步很浪费时间,我个人的习惯是分段同步:
- 先同步
platform/manifest的修改历史。 - 按模块分组同步,比如先同步
art、bionic、frameworks/base、system/core,这些是系统核心。 - 真正需要的外设驱动库和厂商私有库,以二进制形式在编译时才引入。
这种方式在做定向研究时效率最高。如果你只是要读书,完全用不到所有项目。把数万个Git库全部拉到本地,除了维护麻烦,没有任何阅读收益。
分支配对上也要注意,master分支变动太快,不适合稳定阅读,最好选择某个具体的android-x.y.z_rN标签。标签版本是经过一定规模测试的快照,代码细节虽然仍有变化,但整体结构和接口基本稳定,适合学习。
4. 构建系统的组织逻辑:读懂构建就读懂了一半的Android
Android源码里有一类文件让人第一次看的时候很容易心里打鼓:那些.bp、.mk、Android.bp、Android.mk。不少开发者会直接跳过,或者只把它们当作“编译脚本”不予理会。但真实情况是,Android的构建系统是整个源码工程最精密的“路由表”,它规定了每个模块怎么编译、依赖什么、安装到哪里。
4.1 从Make到Soong/Ninja的演进
早期Android使用Makefile体系,以Android.mk为单位描述模块。后来工程越来越大,Make的解析和执行效率都成了瓶颈,于是引入了Soong构建系统,核心配置文件是Android.bp,最终生成Ninja文件来执行具体构建任务。
这里有一个值得深挖的点:Soong对依赖关系的解析是自动化的。你在.bp里声明了shared_libs,Soong会自己推导出头文件路径、链接顺序和依赖传递,而Make时代这些常常要手写。声明式的描述更清晰,但也带来了一个新问题:一旦报错,错误信息往往不如Make时代直接。
我建议读者在第1章这里刻意花一些时间,去读懂四个最基础的构建概念:
cc_binary/android_app/java_library:模块类型定义,分别对应Native可执行文件、应用APK、Java库。name:模块唯一标识,其他模块通过这个名字引用它。srcs:源文件列表,支持通配符。shared_libs/static_libs/libs:链接库的声明方式。
弄懂这四个概念后,你再看frameworks/base里的模块定义,就能配套“书里的代码逻辑”和“构建体系里的模块位置”,比只读逻辑有效得多。
4.2 常用构建目标与产物路径
构建Android源码时,最常执行的命令是m、mm、mma。这三者的区别是很多人的困惑点。简单解释:
m:会把整个源码工程按照依赖关系构建计算出一个目标集合,适合全量。mm:仅编当前目录下的模块,不处理全局依赖。mma:编当前目录下模块,但会同步解决依赖模块。
实际开发建议里,阅读阶段不需要配完整构建,但如果你想改一行源码看效果,mm和mma是效率最高的。还有一个细节容易被忽略:编译产物并非都在同一个目录,system.img、vendor.img、boot.img分别对应不同分区的输出。如果你修改了Framework层代码,通常需要重新生成system.img并刷机验证;改了内核才是boot.img。
这部分和“准备知识”的关系在于,它建立了“源码变更 → 模块构建 → 产物生成 → 设备烧录”这条完备链路。没有这条链路,谈源码理解只能停在纸面。
5. 常见问题与排查技巧实录
每一个读这套书、并且尝试过编译源码的人,几乎都会在一个地方卡住:环境明明照着文档配了,为什么在某个报错上绕了好几天。以下是我自己遇到、以及帮别人处理过的高频问题,整理成一份速查表。
5.1 源码同步中断问题
大仓库同步对网络要求很高,repo sync过程中断是常态。直接重跑repo sync可以继续,但它会重新探测大量项目的状态,效率不高。我通常用repo sync -j4或-j8控制并发数,降低中断概率。如果某个项目反复失败,单独进入该项目执行git fetch和git reset观察,比整体重试更能定位问题。
另外一定要留意磁盘空间:同步过程中临时文件和对象数据会占额外空间,空间不足时repo不会明确给出清晰提示,经常表现为一个无关的项目同步失败。
5.2 源码编译内存不足或耗时过长
如果全量编译系统时内存不足,最常见的报错是编译进程被系统杀掉,日志尾部看似没有明确的错误标记。处理优先级建议:先确认是不是swap空间过小,再确认是否是同时启动的编译任务过多。调低-j数量是最快的缓解方式,但根治还得靠内存和磁盘。
如果磁盘是机械硬盘,全量编译的时间会成倍增长。预算允许的话,把源码目录放到SSD或NVMe上,体验差别是天壤之别。我帮一个朋友排查过,他的编译卡在某个模块超过一小时,换了硬盘之后,同模块编译时间缩短到一个零头。
5.3 源码检索和代码跳转不准确
读第1章之后开始动代码源,大多数人会面对“代码跳转不准”的问题。原因通常是索引不完整,选用的源码版本和使用的IDE的SDK版本不一致,或者缺少部分Native代码的索引。
我的经验是,不要全依赖IDE的“转到定义”。阅读系统框架时,经常需要手动跳转:拿到类名后先在frameworks/base内用文件名索引,再到services目录下找同名Service类。很多默认跳转会指向接口定义,但真正的实现逻辑在Impl、Service或者Native方法里,这种错位对初学者很有迷惑性。
另外,部分JNI方法在Java层看到的是一个native关键字声明,真正的C++实现需要到frameworks/base/core/jni等目录下去匹配。跳转不准确不是工具不好,而是系统代码本身就是跨语言、跨进程的,它不会像普通Android工程一样在一个进程内给你一条直达链路。
5.4 用一份问题速查表收拢高频陷阱
我把这些高频问题整理成表格,方便你对照排查。
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
repo sync中断频繁 | 并发数过高、网络波动 | 降低-j,分段同步,单个仓库单独纠错 |
| 编译被系统杀进程 | 内存或swap不足 | 降低-j,加大swap,优先SSD编译 |
m找不到目标模块 | 未在对应目录执行,或模块名拼写不一致 | 先cd到模块目录,用mma验证 |
| 代码跳转到接口而非实现 | 框架层接口与实现分离 | 手动查找同名Service/Impl/Native实现 |
| Java版本不匹配 | 使用了过新的JDK | 根据源码版本选择匹配的JDK版本 |
| 系统应用编译后无变化 | 未重新生成对应镜像 | 确认修改归属分区,重新生成对应img |
这张表不是标准操作手册,但它是实操中高频出现的真实场景。每一条背后都有一到两天的排查成本,提前扼杀它们,能让你把时间真正花在读代码上。
6. 阅读过程的一点个人体会
再往后走,就是你真正的“读源码”之路了。第1章提供给你的不是一个完整的答案,而是一把钥匙。它帮你认识到,Android系统源码不是一个可以靠线性阅读全部读完的东西,它是由数千个模块组成的网络。真正高效的方式,是先建立地图,再攻据点。
我在实际工作中看到过两种极端:一种是完全不看准备工作,直接从AMS开始逐行读,读完就忘了读过的内容;另一种是过度准备,下载全套源码、配好完整构建、做了所有索引,折腾两周后还没开始看代码。这两种都是“用战术上的勤奋掩盖战略上的懒惰”。
我自己的习惯是:先花半天把源码目录结构画成一张自己理解的脑图,再挑一个最熟悉的模块进去读,比如从ActivityThread到ActivityManagerService的调用路径。读完这条路径,就会反过来理解Binder到底在哪里出现、系统服务进程和应用进程的边界到底在哪里、源码目录为什么这样切分。有了这些感性认识,再回头翻第1章,会有一种“原来当时写的是这个意思”的顿悟感。
最后分享一个小技巧:阅读源码时,随时记录“这个类在哪个进程运行”“它是服务端还是客户端”“它的边界是谁”。这三行笔记积累多了,你会发现自己对系统级的理解已经不是零散的。
说到底,“准备知识”这一章不是拿来背的,也不是读完就完的。它适合在你每一次迷路的时候,重新回来翻一翻。