☰
安卓 AOSP 概览:从源码树、编译镜像到真机跑通
2026/11/6 4:20:07 网站建设 项目流程

安卓 AOSP 这个词,很多人第一次撞见是在招聘 JD 或者一篇讲系统定制的文章里,顺手一搜,蹦出来一堆 repo、lunch、Soong、img 文件,越看越糊。我第一次同步 AOSP 源码是在一台只有 16G 内存的旧机器上,repo sync挂了一整夜,第二天早上发现卡在某个仓库纹丝不动,日志里全是重试。后来才明白,安卓 AOSP 概览这件事,关键不在背概念,而在搞清楚三件事:这棵树长什么样、它怎么被编译成镜像、编出来的东西怎么跑起来。AOSP 全称 Android Open Source Project,是一套完整的、可自行编译的开源系统源码树,包含框架层、原生库、HAL 接口定义、构建系统和一部分系统应用。它能让你从零产出一套可启动的系统镜像,能让你读懂厂商定制系统到底改了哪里,也能让你在排查一个 framework 层 bug 时不至于两眼一抹黑。这篇内容适合三类人:想入门系统层的应用开发者、做定制系统的工程师、以及单纯好奇“手机开机那几十秒发生了什么”的技术爱好者。下面我按自己的实操顺序,把这棵树从概念到跑通完整拆一遍。

1. AOSP 的边界:它是什么,又不包含什么

1.1 Android、AOSP、厂商系统三者的关系

这三个词被混用得最厉害。Android 是一个品牌和一套技术规范,AOSP 是这套规范的开源实现,厂商系统则是在 AOSP 之上做二次开发的商业发行版。用做饭打比方:AOSP 是面粉和基础配方,厂商系统是加了各自浇头的成品面包,而那些预装的商业服务套件属于配菜,不在面粉里。

具体到代码层面,AOSP 提供了frameworks/base(框架层)、system/core(init、logd 等核心守护进程)、bionic(C 运行库)、art(运行时)、system/sepolicy(安全策略)、HAL 接口定义,以及构建系统本身。它不提供的是:芯片侧的驱动与固件、部分编解码实现、厂商私有的相机与音频算法、以及那套需要授权才能预装的商业应用服务。

理解这个边界非常重要。我见过不少新手把“系统里没有某个功能”直接归因于 AOSP 缺失,实际上往往是厂商裁剪或商业组件未集成。反过来,也有人以为 AOSP 编出来就能直接当日常主力机用,结果发现没有应用商店、没有推送服务、相机只能出个能看的图。AOSP 的目标是“可启动、可验证、可作为基线”,不是“开箱即用的消费级产品”。

提示:判断一个问题该不该在 AOSP 里找答案,先问一句“这是平台能力还是产品能力”。平台能力在 AOSP,产品能力在厂商层。

1.2 AOSP 在产业链里的位置

一颗完整的系统从代码到用户手上,大致经过三段:芯片侧提供板级支持包,把内核、驱动、HAL 实现补齐;终端侧在 AOSP 基线上做定制,包括 UI、系统服务增删、预置应用;应用侧在公开接口上开发。

AOSP 处在这条链的中间偏下位置,它定义了上下两端握手的契约。这个契约最典型的就是 HAL 接口:上层框架只认接口,不管下面是谁实现的;芯片侧只要按接口实现,就能被同一套框架调用。这正是为什么同一份 AOSP 代码可以支撑形态完全不同的设备。

对应用开发者来说,懂 AOSP 的实际收益是能读懂“为什么这个 API 在某个版本上行为变了”。很多兼容性问题最后都追到框架层某个方法的实现改动上,而这份改动就明明白白写在源码里。对系统工程师来说,AOSP 是唯一可信的基线,厂商文档经常滞后或省略,源码不会。

1.3 版本节奏与分支命名

AOSP 的版本由主版本号加季度特性更新组成。代码仓库里用标签和分支区分,标签形如android-14.0.0_rXX,末尾的序号代表该版本上的第几次修订,序号越大包含的补丁越新。做产品基线时一般挑某个稳定标签,而不是跟着主干跑。

主干分支适合看最新特性,但不适合做产品。原因很实在:主干每天在变,接口可能今天加明天删,你基于它做的改动会不断冲突。我的习惯是产品基线锁标签,学习看主干,遇到某个特性想看演进历史就用git log追单个文件,比读发布说明直观得多。

还有一个容易被忽视的点:不同标签之间的差异不只是版本号。安全补丁、行为变更、甚至构建脚本的写法都可能在修订之间调整。所以团队协作时必须把基线标签写进文档,别只写“基于 Android 14”,这句话等于没说。

1.4 为什么值得自己啃一遍

哪怕你只做应用层,完整走一遍 AOSP 的同步、编译、刷机流程,收获也远超预期。你会第一次真切理解“系统镜像”不是一个文件,而是一组按分区组织的产物;你会知道logcat里的某条崩溃日志对应哪个进程、哪个 so;你会明白系统应用的权限为什么和普通应用不一样。

更实际的一点是排查效率。一个ANR问题,懂框架的人会去看ActivityManager的调度记录和binder调用耗时;不懂的人只能反复加日志试。差距不在聪明程度,在于有没有那张地图。接下来的内容就是帮你把这张地图画出来。

2. 源码树地图:先认清目录再动手

2.1 顶层目录速查

第一次进源码根目录,看到几十个文件夹确实会懵。按功能归类,常用的其实就那么十来个。

目录作用关注度
build/构建系统核心,Soong、Kati、makefile 都在这里高
frameworks/base/Java 框架层,系统服务与 API 实现极高
frameworks/native/原生层,SurfaceFlinger、binder 库高
system/core/init、logd、adb 等核心工具高
system/sepolicy/SELinux 策略中
bionic/C 运行库与链接器中
art/运行时与编译器中
packages/apps/系统内置应用中
hardware/HAL 接口定义与参考实现高
device/具体设备的配置视情况
vendor/厂商私有代码,通常不完整视情况
external/第三方开源库低
prebuilts/预编译工具链低
out/编译输出,编译后才出现高

我的建议是:先只记住四个位置——frameworks/base、system/core、build、out。前两个是读代码的主战场,第三个是搞不清编译行为时的答案所在,第四个是你每天要看产物的地方。其余目录等真正用到再查,不必一开始就背。

2.2 五层架构与目录的对应关系

从下往上数:Linux 内核、HAL、原生库与运行时、Java 框架层、应用层。每一层在源码树里都有明确归属。

内核在单独的仓库里,不在主树的kernel/下(那个目录通常是空的或只有配置)。HAL 的接口定义在hardware/interfaces/,用接口描述语言写,编译时生成对应语言的桩代码。原生库和运行时在bionic、art、frameworks/native、system/core。Java 框架层在frameworks/base,其中一个services/子目录装着几乎所有系统服务。应用层在packages/apps。

这种分层不只是组织方式,也是权限边界的体现。每层能调用的下层接口是受限的,SELinux 策略会进一步收紧。所以你在框架层随手写一句读设备节点的代码,编译能过,跑起来会被拒。这类问题在开发早期很常见,后面讲排查时会具体说。

2.3 Treble 分界线:HAL 的两种形态

老版本里,HAL 是一个个.so库,框架层直接dlopen加载,库和框架编译在一起,升级框架就得重编 HAL。这套做法在版本升级时极其痛苦,芯片侧要跟着重做一次。

后来引入的分层方案把 HAL 从框架进程里拆出去,变成独立进程,通过 binder 通信(早期是 HIDL,后来统一到 AIDL),并把系统分区和厂商分区彻底分开。这条分界线带来了一个直接后果:系统分区可以单独升级,厂商分区可以独立维护。

对你实操的影响是:如果你在hardware/interfaces里改接口,只重新编译系统侧是不够的,厂商侧的实现也得跟着更新,否则接口对不上,服务起不来。日志里通常表现为某个 HAL 服务反复重启,或者binder调用返回“未知事务”。遇到这类现象,先确认两边接口版本是否一致,比盲改代码高效得多。

2.4 分区与产物的对应文案

编译完成后out/target/product/<设备代号>/下会出现一批镜像文件。搞清它们和分区的对应关系,刷机时才不会刷错。

产物对应分区主要内容
boot.imgboot内核与 ramdisk
system.imgsystem框架、核心库、系统应用
system_ext.imgsystem_ext系统扩展组件
product.imgproduct产品定制内容
vendor.imgvendor厂商 HAL 与驱动相关
odm.imgodm设备定制层
dtbo.imgdtbo设备树叠加
vbmeta.imgvbmeta校验与验证启动元数据

较新的设备还有super.img,它是动态分区容器,把 system、vendor、product 等打包在一起,由逻辑分区机制在运行时拆分。这意味着刷机时你可能只需要刷一个super.img,而不是逐个分区刷。第一次接触时我因为不知道这一点,把老流程套在新设备上,刷完直接不开机。

3. 环境准备与源码同步:最容易卡住的一段

3.1 硬件与系统的实测门槛

官方文档给出的最低配置往往偏乐观,我按自己跑过的几台机器给一组更接近现实的数字。

项目最低推荐说明
内存16G32G 以上16G 必须加 swap,否则链接阶段会被杀
磁盘400G1T SSD源码加.repo约 100G,out视配置 100–200G
CPU8 核16 核以上直接影响全量编译时长
文件系统ext4ext4大小写敏感是硬要求

系统方面用主流的 Linux LTS 发行版,装好基础编译工具链。依赖包大致是构建工具、zip、curl、python3、openjdk、各类lib*-dev。具体清单每个大版本会微调,建议以源码树里build/目录下的环境准备脚本为准,脚本会告诉你缺什么。跑一遍脚本比对着博客抄包名靠谱得多,因为博客的时效性太差。

注意:文件系统必须大小写敏感。在大小写不敏感的分区上同步,会在某些文件名仅差大小写的仓库上直接失败,而且报错信息完全看不出根因。

3.2 repo 是什么,清单文件怎么读

repo不是版本控制工具,它是 git 的一层封装,用来管理几百个仓库的批量操作。它靠一份清单文件描述“这个版本包含哪些仓库、各自对应哪个路径、跟踪哪个分支”。

初始化分两步:先用repo init指定清单仓库地址和分支,再用repo sync把几百个仓库拉下来。第一次init之后,源码根目录会多出.repo目录,里面存着清单文件和 repo 自己的元数据。

mkdir aosp && cd aosp repo init -u <清单仓库地址> -b <分支名> repo sync -c -j8 --no-tags

-c表示只拉当前分支,能省不少空间;--no-tags跳过标签,进一步减少传输量。如果你后面要做版本对比,标签还是有用的,那就去掉这个参数。

清单文件值得花十分钟读一遍。它按路径分组,能看到某台设备的配置放在device/下的哪个位置,能看到哪些仓库指向外部地址。改本地清单可以在.repo/local_manifests/下放自己的 xml,这样上游清单更新时不会覆盖你的改动。这个技巧我用了很多年,比直接改主清单稳妥。

3.3 同步策略与常见卡点

repo sync -j的并发数不是越大越好。我实测过,在千兆网络下-j8到-j16差别不大,再往上反而因为频繁重试变慢。网络不稳定时降到-j4,整体耗时可能更短,因为失败重试的代价很高。

几个常见现象和处理方式:

  • 卡在某个仓库不动:先看是不是大仓库(比如预编译工具链),大仓库本来就慢。真卡死了就中断,单独repo sync <路径>拉那一个。
  • 报 SSL 或连接重置:多半是并发太高。降-j,加重试。
  • 磁盘写满:.repo里的对象库和out目录是两个吃盘大户。清out只解决一时,长期看还是要留够空间。
  • 报 inode 不足:源码树小文件极多,磁盘还有空间但 inode 用完了。df -i一看便知。

同步完成后先别急着编译,跑一次repo status确认工作区干净,再看看build/下的环境脚本能否正常执行。这两步花不了几分钟,能省掉后面一堆莫名其妙的错误。

4. 编译体系:三代构建工具的接力

4.1 Make、Kati、Soong、Ninja 的分工

很多人被这套东西绕晕,其实它们的职责很清晰:Android.bp是新的模块描述格式,由 Soong 解析;Android.mk是旧格式,由 Kati 翻译成 Ninja 规则;两者最终都生成 Ninja 文件;Ninja 负责高效执行。

为什么绕这么大一圈?因为 GNU Make 的语法灵活但慢,Ninja 快但表达力弱。折中方案就是让 Make 和 Soong 负责“描述”,Ninja 负责“执行”。Soong 用 Go 写,解析Android.bp的速度比 Make 快一个量级,这也是新模块都要求用Android.bp的原因。

实操上你只需要记住:加新模块写Android.bp,遇到老模块才看Android.mk;两个文件同名共存时,Soong 会优先,但大多数情况二者不会同时存在。编译报错里如果出现ninja: error,说明执行阶段出问题,去看具体那条命令;如果出现soong或blueprint字样,说明模块描述写错了,去检查Android.bp语法。

4.2 从 source 到 lunch 到 m

第一步是加载环境脚本,它向当前 shell 注入一堆函数和变量:

source build/envsetup.sh

常用函数有lunch(选产品与变体)、m(从根目录编译)、mm(编译当前目录模块)、mmm(编译指定目录模块)、mma(编译当前目录及其依赖)。变体有三种:user是发布版,userdebug是可调试版,eng是开发版。日常开发用userdebug,它有 root 权限和调试能力,性能又接近user。

lunch aosp_<设备代号>-userdebug m -j16

lunch不带参数会列出所有可用目标。选错目标的典型症状是编译能过但刷进去不开机,因为产品配置和设备树对不上。第一次务必确认设备代号与你的目标设备一致。

4.3 加速编译的几个有效手段

全量编译一次动辄一小时,优化手段值得花时间配。

  • ccache:缓存 C/C++ 编译结果,第二次全量编译能快一半以上。设置export USE_CCACHE=1和export CCACHE_DIR=<独立目录>,再执行ccache -M 50G分配空间。注意缓存目录要放在空间充足的盘上。
  • 并发数:-j取 CPU 物理核数的 1 到 1.5 倍比较稳。超线程核数直接翻倍容易触发内存瓶颈,反而变慢。
  • 只编目标模块:绝大多数时候你只改了一两个模块,用mm或mmm就行,没必要全量。
  • 关闭不必要的模块:在产品配置里剔除用不到的应用和组件,能显著缩短首次编译时间。

提示:ccache的命中率和文件路径强相关。如果你把源码树挪了位置,缓存命中率会骤降。所以定好路径就别轻易动。

4.4 增量编译为什么有时候不生效

这是被问得最多的问题之一。明明改了代码,刷进去没变化。常见原因有三类。

第一类是改的文件不在你编译的模块里。比如你改的是frameworks/base下的 Java 代码,但你只mmm了某个应用,那当然不生效。第二类是产物被缓存住了,out目录里存在旧的时间戳。第三类是刷机时没刷对应的分区,最常见的是改了框架但只刷了boot.img。

排查顺序我一般是:确认改动文件的路径属于哪个模块,确认该模块的产物时间戳是新的,确认刷入的分区覆盖了该产物。三步走完基本能定位。还有个更笨但更可靠的办法:改完先做一次m installclean再编,虽然慢一点,但能排除绝大多数缓存干扰。

5. 跑起来:模拟器、真机与日志观察

5.1 模拟器:最省事的验证路径

如果只是想验证框架层改动,模拟器是首选。它不需要真机,不用担心刷坏,回滚成本几乎为零。编译目标选模拟器对应的产品,编完用emulator命令启动,指定系统镜像路径即可。

模拟器的局限也很明显:它没有真实的传感器、相机、蜂窝模块,HAL 层大多是模拟实现。所以涉及硬件交互的改动,最终还是得上真机。我的习惯是:涉及框架逻辑、UI 渲染、权限模型的改动先在模拟器验证,涉及驱动、功耗、外设的改动直接上真机。

模拟器启动慢是常态,冷启动几分钟很正常。给它多分配点内存和 CPU 核数能明显改善。另外注意磁盘镜像会随着使用不断增长,定期清理快照能省不少空间。

5.2 真机:解锁、刷入与风险控制

真机验证是绕不过去的一步。大致流程是:开启开发者选项与调试授权,解锁引导程序,刷入编译产物,首次启动。每一步都有回退成本,尤其解锁会清空数据。

风险控制上有几条硬经验。第一,动手前把原厂固件完整备份一份,最好确认能刷回去。第二,确认设备树与设备型号严格对应,代号差一个字母都可能刷成砖。第三,首次刷入优先用能保留回滚路径的方式,别一上来就全分区覆盖。第四,刷之前把电量充到 60% 以上,中途断电是最坏的情况。

注意:不同设备的解锁与刷写政策差异很大,部分设备不支持解锁,部分解锁后会影响保修或某些功能。动手前务必查清自己设备的具体情况,别拿主力机做实验。

5.3 日志体系:出问题时看哪里

系统跑起来之后,绝大部分排查工作都围绕日志展开。几个入口必须熟:

入口命令或路径典型用途
logcatadb logcat -b all应用崩溃、Java 异常、系统事件
内核日志adb shell dmesg驱动、硬件初始化失败
崩溃转储/data/tombstones/原生层崩溃的完整调用栈
系统事件缓冲adb logcat -b events生命周期、ANR、启动耗时
追踪perfetto采集卡顿、调度、耗时分析

新手最容易犯的错是只看默认缓冲的logcat,漏掉events和crash缓冲。原生崩溃往往只在前者留下痕迹,Java 崩溃在后者。我一般用-b all一把抓,再按 tag 过滤。

还有个技巧:加时间戳和进程号一起看。同一时刻多个进程的日志交错,只看内容很容易误判因果。logcat -v threadtime会带上这两项,排查时序问题几乎是必需的。

5.4 改一行代码之后的小闭环

改完代码怎么最快验证,是效率的关键。我的闭环是:mmm编译目标模块,拿到产物,判断它是.jar、.so还是可执行文件,再决定推送方式。框架层的 jar 通常推到系统对应目录并重启相关服务;原生库推送到lib64或lib下并重启对应进程;可执行文件直接推到/system/bin赋可执行权限。

这套流程快,但有前提:设备必须可写系统分区,通常是userdebug或eng版本,并且关闭了校验。发布版设备上系统分区是只读的,只能重新刷镜像。所以日常开发务必用可调试版本,别拿发布版折腾。

推完记得验证:进程是否真的重启了,加载的是不是新库。用ls -l看时间戳,用lsof看进程打开的库路径,比凭感觉靠谱。我踩过一次坑:推了新库但没重启服务,白折腾半小时。

6. 常见问题排查速查表

把踩过的坑整理成一张表,遇到问题时按图索骥,比从头分析快得多。

现象可能原因排查动作
同步中断在某个仓库并发过高或网络抖动降-j,单独同步该路径
编译报找不到符号模块依赖未声明检查Android.bp的依赖项
编译通过但不开机产品配置与设备树不匹配核对设备代号与产品名
开机卡在启动动画系统服务初始化失败看logcat与dmesg早期日志
某个 HAL 服务反复重启接口版本不一致比对两侧接口定义版本
修改不生效缓存或刷错分区三步确认法(路径、时间戳、分区)
SELinux 拒绝访问策略未放行看avc: denied日志并按需加策略
编译内存不足被杀并发过高或内存太小降-j,加 swap

展开说几个高频的。

开机卡在动画。这是最典型的症状,原因极多。第一步永远是抓早期日志,用adb logcat在开机瞬间连着看,或者直接看dmesg。如果adb都连不上,说明问题在更底层,得用串口或者看内核日志。常见根因是某个系统服务在初始化时抛异常导致system_server反复重启,日志里会看到明显的循环。

SELinux 拒绝。日志里出现avc: denied就是它。处理方法不是简单地设成宽容模式了事,那只是掩盖问题。正确做法是看拒绝的具体上下文,判断是哪个域访问哪个类型缺了权限,然后在策略文件里补规则。补完重新编译策略镜像刷入。临时验证可以先设宽容模式确认因果关系,但绝不能带到发布版本。

编译内存被 OOM 杀掉。现象是编译进程突然消失,没有任何错误输出。dmesg里能看到 OOM killer 的记录。解决办法就两个:降并发或者加内存。16G 机器上我一般用-j4加 16G swap,慢但稳。

增量编译失效。前面提过三步确认法。补充一个细节:如果你切换过lunch目标,out目录里的产物会混在一起,这时候必须做一次clean。混编导致的问题极难排查,因为报错信息往往指向完全无关的模块。

提示:所有排查都建立在“能稳定复现”之上。如果问题是偶发的,先想办法提高复现概率,比如固定操作序列、关闭其他干扰进程,再开始分析。偶发问题的根因分析效率极低。

7. 上手路径与几条实战建议

如果你准备开始,我建议的阅读顺序是:先看system/core/init,理解系统是怎么一步步启动起来的;再看frameworks/base/services/core,理解系统服务怎么注册和调度;然后挑一个具体服务深入,比如负责 Activity 调度的那个;最后再扩展到 HAL 和内核。这个顺序符合“先看骨架再看器官”的认知规律,比随机翻文件高效得多。

代码管理上有两点经验值得分享。一是永远在自己的分支上改,上游更新用repo sync拉下来再 rebase,别直接在工作区改。二是改动尽量按主题拆成小提交,一个提交只做一件事,后面追溯和回退都方便。我吃过一次亏,一个提交里混了三个不相关的改动,出问题时没法单独回退,只能整体撤回重做。

跟上游同步的频率也要控制。太频繁,天天在处理冲突;太久不同步,一次合并几百个提交,冲突到怀疑人生。我的节奏是两周一次,遇到安全相关的更新会提前。同步前先把本地改动提交或暂存,同步后跑一次全量编译确认没坏,再继续开发。

最后说个心态上的事。AOSP 这棵树很大,没有人全部读完,也不需要。我接触这么多年,常看的也就那么几个目录,剩下的都是用到再查。真正的门槛不是知识量,而是知道“遇到某类问题该去哪个目录找答案”。这个能力只能靠一次次实际问题喂出来,看多少概览文章都替代不了。所以别在准备阶段停太久,找个具体的小目标,比如改一个系统应用的默认行为、加一条自定义的系统属性,动手跑通一次完整闭环,比读十篇入门文都管用。

如果非要挑一个最容易见效的起点,我会选“修改一个系统内置应用的界面并让它跑起来”。这条路径覆盖了源码定位、模块编译、产物推送、日志验证四个环节,每个环节都会真实地给你出难题,而每个难题的解法都能复用到后续所有工作里。跑通之后,你对这棵树的感觉会从“一堆陌生目录”变成“一张有坐标的地图”,剩下的就是按需扩展边界了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询