第一次完整同步 AOSP 源码后,我盯着根目录下的那一片文件夹愣了很久。接近三十个名称各异的目录,每个名字都好像在哪见过,真到编译报错、改系统应用、适配新硬件时,又不知道应该往哪个文件夹里钻。后来被构建失败、开机崩溃、测试不通过这些事反复折腾,才算把 AOSP 这套目录体系摸出了门道。这篇就把实战中最常用到的 28 个目录一次性讲清楚,从构建入口到运行核心,从第三方依赖到测试验证,整体过一遍。
1. 先建立整体认知:28个核心目录是怎么分组的
1.1 为什么先看地图而不是先看代码
AOSP 不是一个普通单仓库,它是用 repo 工具管理起来的成百上千个 git 仓库的集合,根目录下每个一级目录基本都有自己的独立历史和构建单元。不同目录之间没有严格的调用约束,像是“微服务”式的代码组织:某几个目录负责生成镜像,某几个目录负责运行时,某几个目录专门提供测试工具。
新手最常犯的错,是拿到源码直接钻进 frameworks/base,然后从某个类开始往上读。结果读了半小时发现一个类依赖了几层 native、一个系统服务又牵扯到 init、property 和 SELinux policy,最后晕头转向。正确做法是先建立一张目录地图:明白每个目录属于哪条线,遇到问题该去哪里找代码,而不是试图一次性读完所有东西。
1.2 28个核心目录速查表
我把这 28 个目录分成四条线:构建与产物、运行核心与框架、第三方依赖与工具、测试交付与应用。严格来说,根目录下同步下来的源码目录是 27 个,还有一个 out 目录是编译自动生成的,但它几乎贯穿你所有实战操作,所以我把它也纳入这个“实战二十八目录”的范畴。
构建与产物线
| 分类 | 目录 | 核心职责 | 实战要点 |
|---|---|---|---|
| 构建与产物 | build | 构建系统引擎,包含 Soong、Blueprint、make 封装、产品打包脚本 | 所有编译命令基本都是从 build/envsetup.sh 进来,先摸底 |
| 构建与产物 | device | 设备与产品定义,包括 BoardConfig、Product 配置、内核配置 | 新增设备第一站,产品名在这里注册 |
| 构建与产物 | vendor | 厂商私有模块、预编译库、设备专有特性 | 开源和闭源的分水岭,闭源驱动基本都落这里 |
| 构建与产物 | kernel | 内核源码和默认内核配置 | 改驱动、调内核开关时关注,实际厂商常用独立内核分支 |
| 构建与产物 | bootable | recovery、bootloader 相关代码 | 做 OTA、刷机方案、改 recovery UI 时重点看 |
| 构建与产物 | prebuilts | 预编译工具链、构建工具、SDK 镜像 | 更新工具链版本时最容易踩坑,版本必须匹配 |
| 构建与产物 | out | 所有构建中间产物和最终镜像 | 不是源码,但排查 80% 的编译问题要先看这里 |
运行核心与框架线
| 分类 | 目录 | 核心职责 | 实战要点 |
|---|---|---|---|
| 运行核心 | bionic | 安卓 C 库、数学库、动态链接器 | 底层崩溃、链接错误排查时绕不开 |
| 运行核心 | libcore | Java 核心类库的 Android 实现 | 要改 java.、javax.行为时使用 |
| 运行核心 | libnativehelper | JNI 辅助代码与桥接层 | 做系统级 JNI 时看这里 |
| 运行核心 | art | 应用运行时:解释器、JIT/AOT、GC | 性能调优、dex2oat 相关,改动影响巨大 |
| 运行核心 | dalvik | 旧虚拟机实现 | 看老代码、兼容历史系统属性时用到 |
| 运行核心 | frameworks | 框架层与核心服务,包含 base/av/native | 系统服务、窗口、媒体、应用框架全在这里 |
| 运行核心 | system | init、属性服务、守护进程、媒体组件 | 开机流程和底层调试必看 |
| 运行核心 | hardware | HAL 接口定义与硬件适配 | 新设备适配、传感器/音视频 HAL 修改的主战场 |
第三方依赖、工具链与文档线
| 分类 | 目录 | 核心职责 | 实战要点 |
|---|---|---|---|
| 第三方工具 | external | 第三方开源库源码,例如 sqlite、protobuf、zlib | 升级依赖库或打补丁时使用 |
| 第三方工具 | abi | C/C++ ABI 兼容规则声明 | 换工具链、调整 libc++ 版本时需要核对 |
| 第三方工具 | toolchain | 编译器工具链源码 | 一般直接使用 prebuilts,源码很少主动去改 |
| 第三方工具 | tools | 开发辅助脚本和工具实现 | 翻自动化脚本、资源处理工具时能挖到宝 |
| 第三方工具 | sdk | SDK 接口定义、API level 配置文件 | 改系统 API 后做兼容性检查时使用 |
| 第三方工具 | docs | 系统源码文档与配图 | 查 API 对照、内部资料时好用 |
| 第三方工具 | developers | 官方开发者示例和 sample 工程 | 学习标准用法最快的入口 |
| 第三方工具 | development | 开发脚本、测试脚本和辅助工具 | 编译辅助、模拟器调试常用 |
测试、交付与应用线
| 分类 | 目录 | 核心职责 | 实战要点 |
|---|---|---|---|
| 测试交付 | cts | 兼容性测试套件 | 过 Google 认证必跑,判断系统兼容性 |
| 测试交付 | test | 测试框架与 VTS 等通用测试定义 | 做驱动/内核级别验证时关注 test/vts |
| 测试交付 | platform_testing | 平台级自动化测试与基准测试 | 改完 framework 后跑回归的好去处 |
| 测试交付 | pdk | 平台开发套件,提供 BSP/产品集成参考 | 做厂商 SDK 或硬件 BSP 时翻阅 |
| 测试交付 | packages | 系统应用、桌面、设置、输入法等 | 定制 ROM 界面和预装应用的主战场 |
这张表先给你一个索引。接下来几章,我把每条线里的关键目录放到真实操作场景里展开讲。
2. 构建与产物线:你每天都在进出的大门
2.1 build、device、vendor 之间的三角关系
接触 AOSP 编译的第一条命令一定是source build/envsetup.sh。这个脚本把build/下的构建工具注册到当前 shell 里,后面的lunch、m、mm、mma、mmm、mma全都是它在背后兜底。很多人只知道照敲命令,不知道lunch选中的产品定义其实来自device/目录。
以新增一台设备为例,你要在device/<厂商>/<机型>/下创建三个关键文件:AndroidProducts.mk注册产品名,<机型>.mk定义继承关系和PRODUCT_PACKAGES,BoardConfig.mk定义板级参数和内核配置。然后再在vendorsetup.sh里加上一句add_lunch_combo <厂商>-<机型>-userdebug,重新执行 envsetup 就能 lunch 到你自己的产品了。
vendor/目录则是厂商私有模块的家。很多芯片厂商的闭源驱动、摄像头算法库、私有 HAL 实现,都会落在vendor/<厂商>/。它们和device/通常是配套的:device 里写配置、声明依赖,vendor 里放真正编译出来的二进制或未开源源码。可以用一个生活类比来描述:build/是中央厨房,device/是菜单,vendor/是私房调料包。菜单上写清食材(PRODUCT_PACKAGES),调料包决定口味(vendor 私有库),中央厨房根据菜单和配方出菜。
有一个经验:当你需要把一个新应用打进系统镜像时,不要只在 packages 下看一眼目录就以为完事了。真正生效的勾选动作通常在产品的.mk文件里,你需要在PRODUCT_PACKAGES中加上模块名。这个模块名来自对应源码目录里的Android.mk或Android.bp中的LOCAL_MODULE/module_name。很多初学者发现“代码明明存在但编译出来的 system.img 里没有”,原因就是产品配置里没注册。
2.2 kernel、bootable、prebuilts、out 的真正角色
kernel/在 AOSP 根目录里更多是一个“默认内核树”,主要用于模拟器、公共参考板。真实设备厂商基本都会维护独立的内核分支,但你在 AOSP 里做的内核配置、defconfig、dtb 打包,和device/<厂商>/<机型>/BoardConfig.mk里的KERNEL_DEFCONFIG、BOARD_KERNEL_IMAGE_NAME有直接关系。改内核后只编 boot.img 时,通常执行m bootimage,产物会落到out/target/product/<机型>/boot.img。
bootable/目录里最常碰的是bootable/recovery。恢复模式不只是刷机用的界面,它是 OTA 升级中的关键一环:校验签名、执行 update 脚本、回滚异常状态。改 recovery 的 UI、按键逻辑、升级流程,都要在这套代码里动。注意 recovery 是独立进内核启动模式的,它依赖的库和主系统不完全相同,别在 recovery 里直接引用了一个只编进 system 的库,否则链接和运行时都会很难看。
prebuilts/的存在一开始会让人困惑:为什么源码树里要塞这么多二进制?答案是构建性能和稳定。AOSP 依赖 GCC、Clang、OpenJDK、Go 工具链、Rust 工具链等一大堆外部工具,如果每次都从源码编译整套工具链,一次全量构建的时间会失控。所以官方直接提供经过验证的预编译工具链,构建系统默认使用它们。实战中真正需要手动动 prebuilts 的场景不多,但偶尔会遇到工具链版本和源码不匹配的情况,比如某次拉取了最新主干代码,但 prebuilts 没有同步更新,链接阶段报一堆奇怪错误,这时第一反应应该是检查 repo 的 prebuilts 目录是否更新完整,而不是怀疑自己的代码。
out/目录则是每个编译者的“事故现场”。常见路径是out/target/product/<机型>/,里面放着system.img、vendor.img、boot.img、obj中间文件、symbol 表等。另一个关键路径是out/host/,里面是跑在开发机上的工具,比如 aapt、adb、fastboot 的 host 版本。比如你改了 framework 层代码,只编m framework,然后可以直接在out/target/product/<机型>/system/framework/framework.jar看到产物。很多“明明编了但没生效”的问题,都是因为没看产物路径、没有重新打包镜像或者没有 push 到设备。
3. 运行核心线:从程序启动到界面绘制的关键路径
3.1 各种“库”和“运行时”的层级
安卓应用进程从启动到跑起来,要穿越的目录其实是一条固定路线。init 进程在system/core/init/里起来,解析 init.rc 后启动 zygote。zygote 代码在frameworks/base/core/java/com/android/internal/os/ZygoteInit.java附近。它 fork 出应用进程后,应用进程运行在art/提供的虚拟机上,加载 DEX 字节码、执行 JIT/AOT 编译、管理对象生命周期。
bionic/是这一整套体系的底层地基。它代替了 glibc,提供 libc、libm、libdl 和动态链接器 linker。应用里所有 Java native 层调用的标准 C 库函数,最后都会落到 bionic。修改 bionic 中的动态链接器,直接影响所有 so 的加载策略,这个目录的改动属于“地震级别”,没有充分的兼容性测试不要轻易碰。
libcore/则是 Java 核心库的 Android 实现。你在 Java 里用的java.lang、java.util、java.io,在 Android 上不是直接照搬 OpenJDK,而是经过适配和裁剪的。遇到java.*类行为异常,应该在libcore/里查问题。比如某个String方法的行为不符合预期,十有八九能在libcore/ojluni或libcore/ojluni/src/main/java找到对应实现。
libnativehelper/稍小,但它是 Java 和 native 之间的桥梁,提供 JNI 相关的辅助结构,比JNIEnv、JavaVM的更上层封装。系统级 JNI 开发、检查 so 里的全局引用,都会和这个目录相关。还有dalvik/目录,它已经是历史遗留,但很多系统属性、权限名、日志 TAG 仍保留着dalvik.或dalvikvm前缀。如果你在 logcat 里看到dalvik.vm.heapgrowthlimit之类的字段,不要以为还是 old Dalvik,实际实现已经迁移到art/,只是属性命名为了兼容性没改。
从职责上理解,bionic管进程的底料,art管执行引擎,libcore管 Java 标准层,libnativehelper管 Java/native 的接口缝,dalvik是旧引擎的墓碑,这五个目录共同撑起一个 Java 应用运行的最小闭环。
3.2 frameworks 和 system 是你动手改得最多的地方
frameworks/是 AOSP 里体量最大的目录,细分下来有三个必须知道的子目录:frameworks/base是框架核心,四大组件、WindowManager、ActivityManager、PackageManager、资源系统都在这里;frameworks/av是音视频、相机、媒体服务的老巢;frameworks/native是 native 服务与 Surface、Input、Sensor 等服务端实现。系统 UI、开机动画、应用安装、权限管理,只要你做 ROM 定制,几乎每天都要在这个目录里进出。
举个例子,要定位SystemServer到底起了哪些服务,直接去frameworks/base/services/java/com/android/server/SystemServer.java。想改开机动画逻辑,去frameworks/base/cmds/bootanimation/。想改通知栏快速设置,去frameworks/base/packages/SystemUI/。这些路径都不是靠背,而是靠一个思路:先找模块,再找服务,再找类。编译时用mmm或mma定位到对应模块,编译后到out/对应路径找产物,逻辑就顺了。
system/目录看起来和 frameworks 很像,但它更接近“底层系统组件”。system/core里有 init、property 服务、logd、lmkd、adbd 等核心守护进程;system/netd管网络权限与路由;system/media管音频策略和媒体相关服务。如果你想分析手机开机的第一个用户进程是怎么来的,看system/core/init和frameworks/base/core/java/com/android/internal/os/ZygoteInit.java的配合。如果你想查某个日志标签是怎么被过滤的,去system/core/logd。
hardware/处于系统和设备之间的夹层。HAL 的接口定义大量集中在hardware/interfaces/,既有 HIDL 也有 AIDL 接口。新硬件接入时,要在这里新增或继承接口,同时要去device/和vendor/配合实现。这个目录的接口改动影响是全局的,一旦改了接口签名,所有依赖该 HAL 的服务和 native 模块都必须同步更新,所以我在做新设备适配时,会先看hardware/interfaces/下面的现有接口能否复用,而不是轻易发明新接口。
4. 第三方依赖、工具链和文档线
4.1 external 与 abi:升级依赖前先看兼容性
external/是把第三方开源项目源码放进 AOSP 的集合地。常见的有 sqlite、protobuf、zlib、libpng、expat、googletest 等。它们以独立源码目录存在,每个子项目通常有自己的Android.bp、Android.mk、OWNERS文件。修改这些库时要特别注意:你并不只是在改“自己的库”,你改的是所有依赖它的模块共同使用的基础设施。比如升级 protobuf 版本,可能影响 binder 传输、演播室、安全补丁等多个模块。
abi/这个目录很多人一辈子没打开过。它不是运行时代码,而是描述 C/C++ ABI 规则的文件集合。当你使用不同工具链版本、修改 libc++ 的普通函数签名、改变结构体布局时,需要核对 ABI 是否还一致。安卓系统高度依赖 prebuilt 二进制库,如果 ABI 发生了变化,原先用旧工具链编出来的库就可能直接崩。我个人的习惯是,但凡要动external/里会被大量 native 模块引用到的库,先跑一遍该目录下的测试,再看abi/里有没有对应符号的检查清单。
另外要提醒一点:external/目录下的代码不一定是“完全开源”。有些子目录只提供头文件或 stubs,真正的实现可能在 vendor 的预编译库里。别看到目录名就默认能编译出东西,先看它的Android.bp和Android.mk中是否有prebuilt属性。
4.2 toolchain、tools、sdk 这些“非业务”目录怎么用
toolchain/存放编译器源码,但它很少被直接编译。日常构建使用的 GCC、Clang 来自prebuilts/,不是从toolchain/现场编出来的。需要动 toolchain 的场景基本是做编译器二次开发、为特定架构增加编译参数、研究内建函数行为。普通 ROM 定制三个月都不会碰它一次,但遇到“一模一样的代码在我机器上编译不过”这种诡异问题时,偶尔要翻一下toolchain/里的配置,了解工具链默认行为。
tools/则是一个更杂的“工具箱”,里面很多脚本是构建流程的辅助。比如一些资源处理、打包校验、自动化测试辅助代码。有时候你会在这里找到意想不到的上层工具实现,因为它们不像 frameworks 一样有清晰的业务边界。我的经验是:先按名字找,找不到就用grep,很多工具类代码比文本文档更能说明一个行业惯例是怎么落地的。
sdk/目录维护 API 接口定义。Android 的 API level、权限组定义、接口白名单都从这里来。做一个 SDK 定制 ROM 时,你需要修改sdk/下的 API 配置文件,否则系统里新增的接口不会被外部应用识别。不过在动手之前先想清楚,绝大多数第三方应用会通过@SystemApi或公开 SDK 来访问系统能力,直接改 API 配置牵一发动全身,通常是厂商系统集成里最吃力不讨好的部分。
docs/、developers/、development/是三类容易被忽略但很有用的目录。docs/存开发文档和配图,适合查系统架构图、接口变更说明;developers/里有官方示例工程,想快速验证一个系统 API 的用法,直接把 sample 拿到本地编译比翻老半天源码更高效;development/更像“开发者工具集”,包含构建辅助脚本、模拟器调试工具、代码检查钩子。很多“我改了源码但 IDE 不认”的工程问题,都可以从 development 目录里找到脚本方案。
5. 测试、交付与应用层
5.1 cts、test、platform_testing 的分工
做 ROM 定制的人最容易忽略测试目录,但真正上量产时,测试目录就是你的“安全网”。cts/是 Google 官方兼容性测试套件,主要验证第三方应用运行时的系统行为是否符合 Android 兼容性定义。过不了 CTS,Google 认证就免谈。它测试的是应用层视角:官方 API 的行为、权限规则、组件生命周期都要和规范一致。
test/目录比 cts 更底层。test/vts是 Vendor Test Suite,专门测 HAL、内核、系统库对设备厂商实现的要求。如果你改了hardware/或kernel/,请务必跑 VTS 相关项。test/下还有构建测试框架、测试工具和 infrastructure 代码,系统中的自动化测试基于它运行。拿它做快速冒烟测试很合适。
platform_testing/则是平台内部的白盒测试。它不像 CTS 那样从外部黑盒验证,而是让平台开发者自己给 framework、system service、perf 行为写测试用例。改完frameworks/base某个服务后,去platform_testing/tests/下找对应测试模块,用atest跑相关测试,比手动刷机验证快得多。
pdk/是 Platform Development Kit,翻译是“平台开发套件”。它包含了很多与产品集成相关的参考文档、模板和工具。厂商做 BSP、做硬件适配、做系统定制时,pdk 里通常有最佳实践和配置模板。它不完全算测试目录,但和“交付给设备厂商”这件事绑定得很紧。如果你要负责把一个 AOSP 系统适配到量产的硬件平台,pdk 就是你绕不开的参考资料。
5.2 packages:定制 ROM 的主战场
packages/是另一个“数量爆炸”的地方,但比 frameworks 简单得多。它主要是系统应用和用户可直接感知的组件:packages/apps/Settings、packages/apps/SystemUI、packages/apps/Launcher3、packages/apps/Messaging、packages/inputmethods等。定制界面、预装应用、修改桌面布局、改设置项,基本都从这里入手。
实际操作中,改 SystemUI 的典型场景是这样的:先改packages/apps/SystemUI/src/下的代码,然后在源码根目录执行source build/envsetup.sh && lunch <你的产品>,接着执行m SystemUI或者mmm packages/apps/SystemUI,编译完在out/target/product/<机型>/system/priv-app/SystemUI/下找到新 APK。如果用模拟器,可以adb root后adb remount,再 push APK 过去,最后重启 SystemUI 进程。Source tree 中的Android.bp决定了模块名和依赖关系,改完不要忘了检查PRODUCT_PACKAGES里是否已经包含该模块。
还有一点容易出错:一些系统应用是用priv-app权限运行的,packages/apps/里很多模块在Android.bp中会声明privileged: true或使用privapp-permissions。盲目把 Application 目录挪到/system/app/可能会导致权限丢失、功能异常。定制预装应用时,尽量沿用原有 product 配置中的安装路径,不要手动改目录位置。
6. 定制和裁剪时,哪些目录能碰、哪些建议少碰
6.1 适合大胆改的地方
从产品定制角度看,device/、vendor/、packages/、hardware/和frameworks/中的部分模块是改动收益最高的地方。改开机动画、系统应用、产品配置、开机脚本,基本都集中在这几个目录。它们互相之间有比较清晰的接口,修改后验证路径也直接:编镜像、刷机、看日志。
比如做一个轻量定制系统,可以为了减少系统镜像体积,从device/<厂商>/<机型>/下的PRODUCT_PACKAGES中移除一些不必要的 app 或模块,再配合PRODUCT_PACKAGES_REMOVE把公共 base 中包含的模块删掉。这种做法比直接删packages/apps/下的源码目录安全得多,因为你只是让某个模块不进入系统镜像,AOSP 源码树仍然完整,后续更新源码也不会因为“目录不存在”而出问题。
6.2 尽量少动的敏感地带
abi/、bionic/、libcore/、art/、dalvik/,这五个目录是“全系统地基”。只要动其中一个,风险评估指数直接翻倍。比如改bionic的 malloc 实现,可能影响所有 native 进程;改art的 GC 策略,可能让某些应用出现莫名的抖动;改libcore的 String 实现,可能引起所有 Java 代码行为变化。不是不能改,而是你必须做好全量测试、兼容性测试、压测的准备。
build/目录也不建议轻易大改。Soong / Blueprint 的构建逻辑非常庞杂,改一个变量可能影响成千上万个模块的编译顺序。普通定制不用碰它,最多在外层产品配置里调整变量。真正的构建开发者才需要深入build/soong,普通人进去容易迷失。
frameworks/base也要注意。目录可以动,但要遵循“增量优先、覆盖优先”的原则。比如你要改某个系统资源的默认值,不要直接修改frameworks/base/core/res/res/values/下面的资源文件,而是利用resource overlay机制,在device/<厂商>/<机型>/overlay/中创建同名资源覆盖。这样后续 AOSP 更新时,你的定制不会每次都跟上游冲突,又可以保证改动集中在一个自己能控制的地方。
6.3 定制节奏的建议
我的经验是:第一轮先只改device/和vendor/,把产品跑起来;第二轮改packages/和frameworks/,做可见的功能与界面定制;第三轮再根据性能和兼容性要求,去评估是否要动art/、bionic/这些底层目录。大部分商业 ROM 的真正差异化都在应用层和框架层,底层改动更多是为了性能调优和内核安全,投入产出比完全不同。
7. 常见问题与排查索引:编译失败时,先翻哪个目录
整理几个我实际踩过、也经常看到同行踩的坑,给你一个快速定位的思路。
第一个最典型:编译时提示No module named SystemUI或类似模块找不到。这时候不要先去盯packages/apps/SystemUI,先去看当前 lunch 的产品定义里有没有把模块加入PRODUCT_PACKAGES。模块编码在Android.bp里是存在的,但产品没有引用,就不会编进 system.img。修改位置通常在device/<厂商>/<机型>/<产品>.mk。
第二个常见问题:链接阶段报一堆undefined symbol。第一反应应该搜索这个 symbol 到底在哪个模块里,用grep -rn "symbol" frameworks/ system/ external/ hardware/之类的方式找出定义。找到定义后,看看当前编译的模块有没有在Android.bp中声明依赖。AOSP 构建系统不允许隐式依赖,少声明一个shared_libs或者header_libs,链接就挂。这个问题和源码逻辑关系不大,几乎全在模块依赖配置里。
第三个问题是开机阶段反复重启,卡在第二屏。先看out/下的构建产物是否完整,尤其确认boot.img、system.img、vendor.img是否都是同一套产物。很多时候是只编译了 system,但内核和 vendor 分区还是旧的,硬件接口对不上导致 init 起不来。然后把logcat和dmesg拿过来,重点看system/core/init相关的 fatal 日志,以及art的运行时崩溃信息。
第四个问题是修改了 framework 服务后,应用拉起来直接闪退。先确认系统服务有没有成功注册,方法是在frameworks/base/services/java/com/android/server/SystemServer.java里看你新增的服务是否放进了 start 流程;再看服务调用时用的 Binder 接口有没有在frameworks/base/core/java里同步更新。AIDL 接口改动后,需要重新编译生成 stub 文件,否则客户端和服务端接口对不上,就会出现ServiceConnection异常。
还有一个和prebuilts相关的经典坑:某次整编之前可能更新过 repo,但 prebuilts 目录没有同步完整,构建时 Clang 报版本不匹配。这种情况没有必要去改本地编译器,回到repo sync把 prebuilts 同步到官方对应 commit,再清理一下out/里的中间产物重新编译,通常能恢复。
结语
真正理解 AOSP 目录不需要背下每个子项目。我个人的经验是先看懂 build、device、vendor 这条构建线,再摸熟 frameworks、system、packages 这条产品线,其他目录在遇到具体问题时再查。等你在 out 目录里翻过几次日志,踩过几次链接错误,这 28 个目录就真正变成你手边工具箱了。下一次遇到“这个改动从哪里下手”的问题,你会很自然地在心里打开这张地图,而不需要再像我当初那样在根目录发呆。