一直折腾安卓系统的人,大概率都会冒出过这样一个念头:哪天真能自己动手,把AOSP源码拉到本地环境里编译出一整套固件,再用fastboot刷进手里的Pixel 4a。源码编译刷机这套流程,表面上听着就是“下载、编译、刷机”三个词,可真到操作的时候,你会同时面对磁盘规划、系统依赖、分支匹配、私有驱动、分区命名这些翻不完的细节,任何一个环节掉链子,都可能让整个下午白忙一场。
这篇记录不是那种一步一条命令的官方文档复读,更多是我自己踩完坑之后的复盘。适合两种人看:一种是想搞明白AOSP到底怎么构建的开发者,另一种是手里正好有Pixel 4a、想刷自己编译系统的折腾党。整个过程我会把关键命令、参数含义、资源需求都拆开讲,尤其是那些文档里不会写、但实操中一定会遇到的细节。
1. 环境准备与磁盘规划
1.1 编译AOSP对机器配置的真实需求
先说结论:编译AOSP最大的门槛不是CPU,也不是内存,而是磁盘空间。很多人一开始只给项目分了200GB空间,结果同步完源码就差不多用掉一半,等到构建产物一出来,直接爆盘。
AOSP完整源码同步下来,仓库体积通常在50GB到60GB之间,这还只是刚拉完代码的状态。真正的体积大头是out目录,也就是构建产物所在的位置。我编译Pixel 4a的sunfish目标,完整构建之后out目录超过180GB,里面光system.img这类镜像文件就占了很大比例。再加上ccache缓存、临时文件、工具链中间产物,整机给到500GB才算从容,我自己后来换了块容量更大的NVMe固态才彻底踏实。
内存方面,16GB属于勉强能进场的配置,编译过程中很容易因为物理内存不足导致进程被系统杀掉。32GB搭配合理的并行任务数会舒服很多。CPU核心数越高,编译时间越短,但核心多了内存压力也会跟着上来,所以不要盲目贪多。
另外比较容易被忽略的是磁盘文件系统挂载方式。AOSP要求在支持文件链接和较长文件名的文件系统上构建,常规的ext4、xfs都没有问题。如果是外部移动硬盘、NAS挂载或者某些特殊挂载参数,很可能在构建过程中遇到莫名其妙的报错,排查起来非常痛苦。
1.2 构建依赖与Ubuntu环境
我用的操作系统是Ubuntu 22.04 LTS,这也是目前构建AOSP主流版本比较顺手的系统版本。Ubuntu 20.04也不是不行,但依赖包版本的差异会带来一些额外麻烦。如果你用的是其他发行版,请先对照官方构建页面确认依赖清单,不要直接照搬Ubuntu的包名。
基础依赖安装,直接一条命令拉齐:
sudo apt-get update sudo apt-get install -y git-core gnupg flex bison build-essential zip curl \ zlib1g-dev libc6-dev-i386 libncurses5 lib32ncurses-dev x11proto-core-dev \ libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip \ fontconfig python3 python-is-python3 openjdk-17-jdk这些包的名字看起来琐碎,但每一个都有用处。flex和bison是词法/语法分析工具,构建过程中很多代码生成步骤依赖它们;lib32系列是为了兼容32位辅助工具;openjdk-17-jdk对应较新的Android版本构建需求,如果你的分支是Android 12之类的老版本,可能要换成openjdk-11。
还有一个小细节:Ubuntu 22.04里默认的python指向python3,但AOSP中不少脚本仍然习惯性调用python。安装python-is-python3这个包,让python命令直接可用,能避免很多脚本第一行就报错的尴尬情况。
2. 源码获取:repo init与同步
2.1 repo工具安装与分支选择
AOSP的代码量不是普通git仓库能驾驭的,它用repo这个多仓库管理工具来组织。repo本身不是编译系统的一部分,它只是一层封装,帮你在几百个git仓库之间统一执行init、sync、status这些操作。
安装repo的常规做法是把它放到一个bin目录下,再把这个目录加进PATH。我的操作是这样:
mkdir -p ~/bin curl -sSL 官方repo工具下载地址 > ~/bin/repo chmod +x ~/bin/repo export PATH=~/bin:$PATH把export那行写进~/.bashrc,这样以后每次登录终端都能直接用repo命令。
分支选择这里有个容易踩的坑:新手看到repo init手册里的master分支就直接用了,但master是AOSP的滚动开发分支,今天编译通过不代表明天也能通过,不适合作为稳定复现的环境。我建议选择一个已经发布的具体tag,比如android-13.0.0_rX这种形式。X的具体数字要看官方manifest里当前维护的版本列表,不要照抄别人文章里的数字,因为老tag可能已不再维护,新tag的编号规则又可能变化。
初始化仓库时这样写:
mkdir -p ~/aosp cd ~/aosp repo init -u https://android.googlesource.com/platform/manifest -b android-13.0.0_rX这里的-u参数指定manifest仓库地址,-b参数指定分支。执行成功后,目录下会出现一个.repo文件夹和manifest文件,后续所有源码仓库都会按照这个清单拉到本地。
2.2 同步源码时的细节与坑
repo init只是建立了一个骨架,真正把几百个git仓库全部拉到本地的是repo sync。第一次全量同步,网络条件正常的情况下大概需要一两个小时,时间长短取决于机房到AOSP官方仓库的实际链路质量,这一步没有捷径。
我强烈建议第一次同步不要开特别高的并发:
repo sync -j4为什么不用-j8或者更高?因为第一次同步时所有仓库都是空的,高并发会把网络链路和服务器端压力一起拉满,碰到限速或者连接重置的概率反而更大。等第一次全量同步完成,后续增量同步再适当提高并发,效果会好很多。
同步过程还有一个有用的参数是-c,它的含义是只同步当前分支的代码,不去拉取其它分支和tags。这样可以显著减少网络传输量和本地仓库体积。如果你不打算在代码历史里来回切换,加上这个参数没有坏处。
另一个重要提醒:工作目录不要用root用户操作。AOSP构建体系里很多脚本对文件权限有要求,一旦用root生成了部分文件,后续普通用户编译会碰到各种权限错乱,反过来再处理会很麻烦。我自己就用单独的普通用户来完成整个流程。
同步结束后,可以看一眼目录体积:
du -sh ~/aosp正常情况下这里会是50GB上下。如果数字明显偏小,不要急着开始编译,多半是某些仓库同步不完整。
3. 驱动二进制包:补齐厂商私有部分
3.1 为什么AOSP默认编不出可用的Wi-Fi和相机驱动
AOSP是Android的开源骨架,但一台手机能正常打电话、连Wi-Fi、拍照录音,靠的远不止开源部分。芯片厂商和整机厂商的硬件驱动、固件、HAL实现,大部分是以预编译二进制形式分发的,这些文件不会进入AOSP主线。
如果不补齐这部分,编译过程能顺利通过,但刷进机器后你会面临很尴尬的局面:系统能开机,Wi-Fi无法启用,相机打不开,音频可能是哑的。第一次踩这个坑时,我一度以为是编译参数问题,反复重编了好几次,后来才反应过来是驱动二进制没有集成。
Pixel 4a这类设备的驱动,设备厂商会以独立脚本的形式提供给开发者。下载解压后会得到两个shell脚本,一个负责设备专属二进制,另一个负责vendor分区内容。把这两个脚本放到源码根目录依次执行,它们会把私有库、固件、配置文件按预定义的路径拷贝到源码树中,后续构建时自动打包进对应的分区镜像。
3.2 下载与执行驱动脚本的注意事项
驱动脚本的下载页面会和AOSP分支tag对应起来,选择的时候要确保驱动版本和你选定的Android版本完全匹配。一个典型的错误是源码分支用了新的tag,驱动还是上一版的,结果编译时某些模块提示找不到对应符号或者版本不匹配。
把脚本放到源码根目录后分别执行:
./extract-google_devices-sunfish.sh ./extract-google_devices-vendor.sh执行过程中脚本会打印一段很长的协议内容,最后停下来询问是否接受,输入I ACCEPT并回车,脚本就会把对应文件释放到源码树里。文件拷贝完成后,建议重新运行一次repo status确认没有意外的文件冲突。
需要特别说明的是,驱动版本和系统版本的匹配没有模糊余地。假如你第一次编译用的驱动没问题,之后切换到了新的AOSP分支,必须回到驱动下载页获取对应新版本的驱动包,重新执行一次脚本,旧驱动文件通常会覆盖到同目录,但不会替你清理残留,必要的时候可以先清掉vendor目录下的历史产物再跑脚本。
4. 编译阶段:从lunch到make
4.1 加载编译环境与lunch选择
源码和驱动都准备妥当,接下来进入核心编译流程。首先加载编译环境:
source build/envsetup.sh这个脚本会向当前shell注入大量构建相关的函数和变量,之后你才能使用lunch、m、mma这些快捷命令。加载成功之后,运行lunch不带参数会列出一大堆可选的构建目标,也可以直接指定:
lunch sunfish-userdebug解释一下sunfish这个代号。Pixel 4a的内部代号就是sunfish,AOSP的设备配置目录、内核匹配、分区表都是围绕这个代号组织的。后面紧跟的userdebug是构建类型,AOSP里常见三种:
| 构建类型 | 主要特点 | 适用场景 |
|---|---|---|
| user | 接近正式版,权限收紧,没有root | 体验接近零售系统 |
| userdebug | 带root能力,含调试工具,性能较好 | 日常开发、刷机尝鲜的首选 |
| eng | 工程版,权限最大,包含大量调试辅助 | 底层调试、驱动开发 |
我日常开发用userdebug最多,它既保留了adb root和remount这些调试能力,系统整体表现也没有明显劣化。eng版本虽然权限最全,但后台服务多、运行效率差,不适合长时间当作日常系统用。
4.2 并行任务数与内存之间的关系
lunch之后直接执行make命令就可以开始编译,但并行任务数是个必须自己控制的参数:
make -j8很多人上来就make -j$(nproc),服务器上几十个核心全部拉满,编译确实快,但内存很快告急。实际编译时每个并行任务的进程都可能消耗1GB到2GB内存,如果物理内存只有32GB,开个-j32就是给自己挖坑,系统内存一耗尽,编译进程被内核杀掉,再恢复起来非常浪费时间。
对于32GB内存的机器,-j8到-j12比较稳妥。如果你用的是物理核数较少的写入型机器,按nproc的一半再低一点来设置都合理。编译过程中可以用htop实时观察内存剩余情况,看到Swap疯狂读写就别硬撑了,趁早中断调整参数。
顺手把ccache也配置一下。ccache可以对C/C++编译结果做缓存,AOSP项目后续多次增量编译时差别很大。第一次构建前先设置缓存上限:
export USE_CCACHE=1 export CCACHE_EXEC=$(which ccache) ccache -M 50G这里把缓存上限设为50GB,实际大小取决于构建规模。建议把这两个export变量写进~/.bashrc,否则每次新开终端都要重新设置。
首次全量构建,机器配置中上等的情况下,大约需要四到六小时。不要把首次构建当作常态来看,后续增量编译通常只要十几分钟就能完成一个模块的验证。
4.3 认识输出目录与核心镜像
编译结束后,进入out/target/product/sunfish目录,你会看到完整的产物清单。其中几个核心镜像需要搞清楚用途:
| 镜像文件 | 内容 | 刷机时的作用 |
|---|---|---|
| boot.img | Linux内核与ramdisk | 负责系统启动核心 |
| init_boot.img | 独立初始化ramdisk | Android 13开始的内核启动阶段 |
| dtbo.img | 设备树覆盖片段 | 让内核适配具体硬件配置 |
| system.img | 系统主体 | 框架、应用、库文件 |
| vendor.img | 厂商私有模块与HAL | 硬件驱动的运行环境 |
| product.img | 产品定制内容 | 设备特有应用与配置 |
理解这些分区各管一摊很重要,因为刷机时如果只刷了boot和system,没有刷vendor,设备会是半瘫痪状态。同样,只刷vendor却忽略了system版本匹配,也可能导致系统服务崩溃。
构建产物里还有一层super.img的概念,现在不少新设备把system、vendor、product这些分区打包进逻辑分区。Pixel 4a刷机时可以直接逐个刷镜像文件,前提是你清楚设备当前处于哪种分区模式。这一点我会在刷机部分展开说明。
5. 刷机与验证
5.1 解锁Bootloader的正确姿势
编译完成只是第一步,把镜像刷进设备是另一个考验。刷机的前提是Bootloader已解锁,而解锁会清空设备所有数据,这一步在操作前要有心理准备。
首先在Pixel 4a的系统设置里找到开发者选项,打开OEM解锁开关。如果这一项是灰色不可点,说明设备层面限制了解锁操作,这种情况常见于运营商定制版或部分二手设备,后面我会单独讲。
确认开关可以打开后,用数据线连接电脑,执行:
adb reboot bootloader设备会进入fastboot模式,屏幕显示状态信息。此时在电脑上执行:
fastboot flashing unlock设备屏幕上会出现确认提示,用音量键选择解锁,电源键确认。这一步完成后设备会恢复出厂设置并重新启动,之后需要再次进入fastboot模式才能刷入镜像。解锁过程只做一次,之后Bootloader会保持解锁状态,想要重新上锁再用fastboot flashing lock再锁回去,但每切换一次状态都会清数据。
5.2 逐分区刷入编译产物
设备进入fastboot模式后,先确认电脑端能识别设备:
fastboot devices正常会看到一行设备序列号,如果没有输出,多半是驱动或数据线问题,可以先换一根线试。
接下来按顺序刷入编译产物。bootloader和radio镜像不在AOSP构建输出里,我不会去动它们,只刷构建出来的分区:
fastboot --slot all flash init_boot out/target/product/sunfish/init_boot.img fastboot --slot all flash boot out/target/product/sunfish/boot.img fastboot --slot all flash dtbo out/target/product/sunfish/dtbo.img fastboot --slot all flash system out/target/product/sunfish/system.img fastboot --slot all flash vendor out/target/product/sunfish/vendor.img fastboot --slot all flash product out/target/product/sunfish/product.img fastboot --slot all flash system_ext out/target/product/sunfish/system_ext.img--slot all这个参数会同时刷入A/B两套槽位,避免当前槽位和备用槽位不一致导致后续系统更新混乱。如果你创建镜像时只针对单槽位,也可以不带这个参数,但all策略更省心。
全部镜像刷完后,还有一个容易被忽略的步骤是清除metadata分区:
fastboot erase metadata这个分区的数据相当于系统的动态分区元数据状态,直接沿用旧数据或者新旧固件版本不一致,很容易导致开机后挂在分区挂载阶段。清除之后执行:
fastboot reboot设备会正常重启进入系统。第一次开机可能比正常情况下慢,属于正常现象,毕竟userdebug版本本身带更多调试逻辑。
5.3 第一次开机的验证思路
系统起来之后,不要急着欢呼,先做几个基础验证,确认这确实是刚编译出来的版本。
用adb连接设备后,查看编译版本信息:
adb shell getprop ro.build.version.release adb shell getprop ro.build.version.incremental adb shell getprop ro.product.devicerelease显示Android大版本号,incremental显示编译的增量标识,ro.product.device的结果应该是sunfish。这三个值同时对上,才说明当前运行的固件是你本地构建的产物而不是之前残留的旧系统。
再检查一下Wi-Fi能否打开、相机是否能调用、声音是否正常。因为私有驱动模块是否完整集成,只有通过这些实际功能才能验证。如果某个硬件功能失效,优先检查驱动二进制包有没有正确执行,而不是怀疑编译参数错了。
6. 常见问题与排查技巧实录
6.1 磁盘空间不足与out目录瘦身
编译到一半空间不够,是最常见也最让人头疼的问题。当你发现df -h里根分区已经逼近100%,而编译进程还在继续,系统很可能在接下来几分钟内报错中断。
这时候不要急着去删除别的目录。out目录里有大量可以安全清理的临时文件,最直接的方式是:
make clean这个操作不会删除已生成的最终镜像,但会把中间产物清理干净,腾出的空间相当可观。如果构建问题没结束,还可以用make clobber做一次更彻底的清理,代价是下次增量编译会退回到接近全量构建的状态。
更好的方案是从源头规划:磁盘至少留出300GB专门给AOSP,并且定期检查out目录占用。ccache的缓存也会持续膨胀,如果磁盘紧张,可以调小ccache -M的数值。
6.2 内存不够导致编译进程被杀死
症状很典型:编译过程中突然报错,日志尾部出现"Killed"字样,或者htop里看到进程数量急剧减少。这就是系统OOM Killer在干活了,说明单个编译进程消耗的内存已经超过物理内存总量。
排查思路是先确认自己的可用内存:
free -h然后根据剩余内存调整make的并行数。如果机器上同时跑着其他服务,还要把它们的占用也算进去。另一个方式是通过swap缓解部分压力,但项目规模大时swap读写会成为性能瓶颈,只能解燃眉之急。
需要提醒的是,降低并行数意味着编译时长增加,不要简单以为重启编译程序就能恢复状态。构建系统会在断点处继续,大部分中间产物都还在out目录,中断后重新执行同样参数的make,会优先复用已有产物。
6.3 repo sync莫名其妙中断
同步源码中途断掉是常态,网络波动或者远端压力过大都会触发。有些人误以为这是环境问题,急着重新初始化,其实只需要再执行一次同步命令就好:
repo sync -j4repo工具本身支持断点续传,已经同步完成的仓库会跳过,没有完成的仓库重新拉取。如果连续几次都在同一个仓库断掉,多半是该仓库体积太大或者与远端连接不稳定,可以单独把那个仓库再同步一次:
repo sync -c 仓库路径如果一直折腾很久都过不去,检查一下本地网络环境是否稳定,以及防火墙有没有对长连接做限制机制。
6.4 刷机后卡开机界面循环
刷完系统重启,屏幕上一直转圈或者反复重启,这种情况多数不是镜像本身坏了,而是分区数据不干净。回到fastboot模式,把之前提到的几个关键步骤重新做一遍:
fastboot erase metadata fastboot erase userdata fastboot rebootuserdata存放的是用户数据和启动状态,构建系统切换之后旧数据往往和新固件不兼容。这一步会清空所有数据,相当于给新系统一个干净起点。我遇到过不少“刷机变砖”的求助帖,最后都是清掉数据分区就解决的,真正需要重新刷bootloader的情况少得多。
6.5 需要特别注意的分区与解锁风险
Pixel 4a设备上,bootloader和radio的解锁、刷入有硬件层面的限制,不像system分区那样随意。如果你的设备在开发者选项里没有OEM解锁开关,或者解锁命令执行后提示错误,说明这台设备被销售区域或运营商策略锁死了。这类设备不要试图用非常规方式强行解锁,也不要刷入从别处下载的bootloader镜像,风险是直接把引导分区写坏,设备就真的救不回来了。
另外提醒一点:每次刷入新构建前,优先确认自己刷的是哪个槽位。A/B设备默认从当前槽位启动,如果把镜像只刷到一个槽位,而当前启动的却是另一个槽位,看起来就像升级没生效。用--slot all可以在绝大多数场景下避免这个坑。
最后我再分享一个切身体会。第一次编译AOSP的时候,我全程盯着终端输出,那种看着几百个编译任务一个个跑完的成就感,和下载现成固件刷进去完全不是一回事。虽然这套流程在高配置机器上也要好几个小时,中途还会遇到各种预料之外的问题,但正因为每一步都亲手处理过,之后再碰到Android系统相关的疑难杂症,我不再是盲猜,而是能顺着源码、产物、分区之间的关系推理出问题所在。
如果手头正好有Pixel 4a,又想体验一下从源码到真机的完整链路,照着这条路径走一遍,收获不会小。编译好的镜像不一定是最好用的系统,但一定是你对它底层结构最熟悉的一套系统。