AOSP编译刷机实战:从源码到Pixel 4a的真机之旅 1. 为什么要折腾AOSP编译刷机一次真实的Pixel 4a入坑记录最近把一台放在抽屉里吃灰的Pixel 4a翻了出来决定从AOSP源码开始亲手编译一版安卓系统并刷进去。整个过程从环境搭建、仓库同步、系统编译、Bootloader解锁到最后的刷机与开机验证前后折腾了大概两天。这篇记录写给所有想入坑AOSP编译刷机的朋友尤其是手里正好有Pixel系列设备的人——这套流程几乎可以照搬。AOSP的全称是Android Open Source Project简单理解就是安卓系统的源代码本身。自己编译AOSP意味着你可以决定系统里装什么、去掉什么还能临时修改系统代码然后直接跑到真机上验证。对于做系统开发、逆向分析、定制ROM的玩家来说这是一条绕不开的路。Pixel 4a的代号是sunfish作为安卓原生设备它的硬件适配和驱动闭源程度相对可控用来做编译刷机实验是最合适的硬件平台之一。这篇文章不打算用教科书的口吻讲理论只讲实际操作。我会把所有步骤拆开说清楚每一步在干什么、为什么这么做、有哪些坑尽量做到你看完就能自己动手。2. 编译前奏先把机器、系统、工具链全部捋顺2.1 硬件底线磁盘、内存、CPU一个都不能少AOSP整个项目编译对硬件的要求是出了名的能吃。先算一笔账完整同步AOSP官方主要分支的源码需要大约30GB空间编译过程中生成的out目录又会膨胀到100GB以上如果开了ccache还得额外准备几十GB。我这次给Pixel 4a编译Android 13分支最终源码目录加out加ccache总共吃了接近200GB。建议直接准备250GB的可用空间宁多勿少。磁盘类型上强烈建议NVMe固态硬盘。机械硬盘不是编译不了而是同步源码时大量小文件的IO会慢到让人绝望编译阶段的文件写入同样会拖后腿。同样的任务NVMe固态大概能把总耗时压缩到机械硬盘的三分之一甚至更少。内存方面16GB算及格线32GB更舒服。低于16GB的机器在编译最后的链接阶段大概率会看到“Killed”字样这是操作系统内存不足时的自我保护机制直接就把编译进程杀了没有任何商量余地。如果确实只有8GB或16GB内存也不是完全没法用可以开swap但编译速度会明显下滑。CPU核心数决定了并行编译的上限make -j后面的数字可以按核心数来。我这次用的是8核16线程的处理器综合耗时在1小时20分钟左右供参考。2.2 操作系统选择Ubuntu 22.04 LTS的正确打开方式AOSP官方长期支持和验证的开发环境是Ubuntu LTS版本我用的是22.04 LTS。20.04 LTS也没问题Android 13分支对这两者都在支持范围内。Windows和macOS不是不能做但会遇到更多环境兼容性问题尤其是文件系统大小写敏感和依赖链完整性的问题所以不建议新手在非Linux环境碰这个项目。装完系统之后先把基础依赖一次性装齐。这里给一份我这边实测没有缺包的命令sudo apt update sudo apt install -y git-core gnupg flex bison build-essential zip curl zlib1g-dev \ libc6-dev-i386 libncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev \ libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig python3 openjdk-11-jdk这些包里有些是老一代编译链需要的比如lib32z1-dev和libncurses5-dev编译老分支时会用上新分支虽然依赖有所减少但装齐了一劳永逸。需要特别说的是OpenJDK版本不同Android分支对JDK版本有硬性要求Android 13附近的分支基本上用OpenJDK 11没问题。如果你编译的是更早的Android 8/9需要的可能还是OpenJDK 8这个要提前看清你要同步的分支说明。然后安装repo工具。repo是AOSP源码管理器本质上是对git仓库集合的一层封装专门用来管理Android这种“几百个git仓库组合成一个项目”的形态。Ubuntu的软件源里有repo包直接装sudo apt install -y repo安装完成后可以执行repo --version确认一下。如果不用发行版自带的repo也可以从官方渠道下载repo脚本放到~/bin目录下注意把~/bin加到PATH中。这两种方式我都试过发行版自带版本通常够用但如果你发现repo命令的选项和文档对不上就换成官方脚本方式。2.3 同步前的两个小优化文件监视限制与镜像加速这一小节讲两件小事但它们真的会在后面节省你大量时间。第一件是inotify限制。repo sync会创建大量文件和目录在部分Linux发行版默认配置下可能出现inotify实例数量或监视数量不足的报错。提前调高上限echo fs.inotify.max_user_watches524288 | sudo tee -a /etc/sysctl.conf echo fs.inotify.max_user_instances1024 | sudo tee -a /etc/sysctl.conf sudo sysctl -p第二件是镜像加速。AOSP的全部代码托管在官方源码服务器上在部分网络环境下直连下载速度可能非常慢而且几十GB的数据量经不起反复断线。通用的做法是在repo init阶段就把manifest地址换成可用的镜像源。这个我建议直接搜索当前的AOSP镜像加速方案选择文档更新及时、同步延迟低的源。需要注意不同镜像源的更新策略不同有的隔几小时同步一次有的隔天同步一次。如果你需要编译的是特定分支确认镜像源上已经包含这个分支再开始同步否则会在后面卡在“分支不存在”这种问题上。环境这关过了接下来就是真正的耐力题——源码同步。3. 源码同步repo sync是一场深夜长跑3.1 初始化manifest选对分支很关键开始之前先创建一个干净的目录用来放整个AOSP代码树mkdir -p ~/aosp cd ~/aosp然后是初始化命令核心是repo init后面的分支参数。以Pixel 4a为例这台设备官方支持的最高安卓版本是13所以我选了Android 13的分支。一个典型的分支写法是这样的repo init -u manifest仓库地址 -b android-13.0.0_r16如果你用的是镜像源把尖括号里的地址换成镜像源提供的manifest地址即可。-b指定的是manifest分支也可以理解成你要跟踪的安卓大版本。这里有一个很容易被忽略的点-b后面的分支名决定了你整个代码树的基础版本。选得太旧会遇到老系统对JDK、工具链版本的各种挑剔选得太新又可能超出设备官方支持范围驱动和底层固件对不上。对Pixel 4a来说Android 13就是比较均衡的选择。如果不知道自己有没有选对初始化完之后可以看一眼.repo/manifests目录里面就是当前分支的manifest文件。打开manifest可以发现每个仓库对应的远端地址和revision都定义好了这也是后面同步的依据。3.2 开始同步让代码自己跑一晚同步的命令很简单但等待过程很考验耐心repo sync -j8 -c-j8表示并发拉取8个仓库这个数字可以根据网络带宽调节带宽充足可以加大。-c表示只拉取当前分支不拉取其他分支的引用能省不少空间和时间。第一次同步的数据量通常有二三十个GB取决于分支和仓库数量。我建议晚上挂机同步白天继续编译把时间错开。同步过程中即便某几个仓库失败repo也会尝试继续最后会在终端列出失败的仓库。这时不用慌重新执行一遍repo sync -j8就好它只会补齐失败的仓库不会把已经同步好的重新拉一遍。反复失败的话考虑是不是网络中断太频繁或者并发数设置太高。同步完成后用du -sh .看一眼代码目录总大小会非常有成就感。此时整个AOSP代码树已经就位可以到frameworks/base目录里git log --oneline -1之类的命令确认一下版本状态。3.3 同步完成后先做什么确认目标产物命名代码就位后先别急着编译。有一个习惯值得养成先到源码的设备配置目录里确认Pixel 4a的build target是否真的存在。AOSP把各设备的配置放在device和vendor相关的目录里sunfish这个设备配置通常由厂商以预编译目录的形式提供。这里的坑在于如果你只同步了AOSP主线而缺少对应的设备支持目录编译目标列表里就不会出现aosp_sunfish后面lunch的时候会发现根本没有这个选项。确认的方法很简单在AOSP根目录执行ls device/厂商目录/sunfish 2/dev/null || echo not found具体厂商目录名由分支决定认准sunfish这个设备代号就行。如果没有需要检查manifest里是否包含对应仓库或者是否选错了分支。这个问题我遇到过不止一次都是因为选了精简的manifest分支。AOSP主线是有设备相关仓库的一般不会有问题但“确认目标存在”这一下几秒钟的操作可以帮你避免几十分钟的无效等待。源码就位的下一步是进入整个流程里计算量最大的环节编译。4. 编译系统从source到make的完整通关记录4.1 加载编译环境和选择构建目标在AOSP根目录执行source build/envsetup.sh这行命令会把编译系统需要的环境变量、函数加载到当前shell里包括后面要用的lunch、m、mm等命令。注意这里必须用source不能直接执行因为子进程里export的环境变量不会保留到当前shell。接着选择构建目标lunch aosp_sunfish-userdebug如果你不确定设备代号直接敲lunch回车它会把所有可用的target列出来翻页找带sunfish字样的即可。target名字的格式是产品名-版本类型版本类型主要有user、userdebug、eng三种。user版本和正式发布版本一样没有root权限userdebug版本在user基础上加了调试支持和root能力适合日常开发调试eng版本带完整的调试符号和更多调试工具但体积最大、运行时性能也略差。个人编译刷机玩我强烈推荐userdebug既能保留真实系统体验又能用adb root快速调试。lunch完成之后终端会输出当前target的构建参数摘要包括ABI、编译器路径等信息看到这些说明环境加载正常。4.2 开ccache给下次编译留条后路ccache是编译缓存工具原理是把编译产生的中间文件按输入内容哈希后缓存起来下次编译时如果输入没变就直接用缓存跳过重复编译。对AOSP这种动辄几十万文件的构建工程增量编译有没有ccache效率差别非常明显。开启方式是在编译前设置环境变量并把缓存目录初始化export USE_CCACHE1 export CCACHE_DIR$HOME/aosp-ccache prebuilts/misc/linux-x86/ccache/ccache -M 50G-M 50G是给缓存设置50GB的上限具体大小看你磁盘余量。我把缓存设到50GB实测到后面增量编译时系统镜像部分的复用率很高改动框架代码后再编译从一小时级别降到十分钟级别完全有可能。需要提醒的是ccache目录占用的空间是实打实的别设得太大挤爆磁盘。另外切换分支后ccache的命中率会明显下降因为源码版本变了缓存key基本都会失效。4.3 make编译把代码变成镜像的过程环境准备好之后就是整个流程里最核心的一条命令make -j$(nproc)$(nproc)会自动读取当前机器的逻辑核心数。并行数并不是越大越好如果核心数和内存不匹配开满线程反而会因为内存不足频繁卡顿。8核16线程的机器除非内存有32GB否则我建议并到一个适中值比如make -j8或make -j12更稳妥。第一次编译耗时长短取决于机器性能快的半小时慢的三四个小时都正常。编译过程中终端会不断滚动输出主要内容是ninja构建的子任务进度。如果看到终端底部有FAILED开头的错误说明某个子任务失败了这时候不要只看最后一行要往上翻找到实际的编译或链接错误信息通常会有明确的文件路径和报错原因。4.4 编译产物与关键产出目录编译成功后所有镜像都会出现在下面的目录里out/target/product/sunfish/主要产物逐个说清楚。boot.img是启动镜像包含内核和ramdisk缺少它设备没办法进入系统dtbo.img是设备树覆盖镜像专门描述硬件设备在运行时的板级差异vbmeta.img是Android Verified Boot验证元数据刷入时会和Bootloader的验证机制配合system.img、product.img、vendor.img对应系统各个分区super.img则是动态分区的总集成镜像Pixel 4a的system、product、vendor都作为动态分区存在于super里。另外注意out目录下还有个host工具目录。编译过程中产生的fastboot、adb等主机工具都在out/host/linux-x86/bin/下面。如果系统自带的fastboot版本不够新优先用这里生成的避免版本不匹配导致刷机失败。这也是一个容易被忽略但很关键的点。第一次编译成功时终端会出现类似### build completed successfully的提示。看到它说明你离真机运行自己编译的系统只差最后一道刷机工序了。5. 刷机实操从解锁到把系统装进Pixel 4a5.1 刷机前的准备备份、电量、驱动缺一不可刷机前最关键的一句提醒解锁Bootloader和刷机都会清空手机数据。如果你平时用手机拍照、聊天、存文档请先把需要的东西全部备份出来。Pixel 4a的解锁行为意味着数据无法保留这是硬性规则没有任何绕过空间。电量方面建议把手机充满到80%以上再接电脑刷机过程中断电或者数据线松动后果都比较麻烦。线材也建议用原装或质量可靠的数据线。再确认一下开发者选项里的OEM解锁是否可用。路径是设置-关于手机-连点版本号七次开启开发者模式然后进入设置-系统-开发者选项打开OEM解锁和USB调试。如果在某些特殊版本或状态下手机会拒绝解锁界面会直接提示原因这种情况就不要强行继续了。5.2 解锁Bootloader踏出刷机的第一步手机开启USB调试并连接电脑后先用如下命令确认设备识别adb devices如果设备列表里出现了设备号且状态是device说明连接正常。若显示unauthorized需要在手机上确认调试授权弹窗。接下来让手机重启进Bootloaderadb reboot bootloader进入Bootloader界面后确认fastboot识别设备fastboot devices解锁命令如下fastboot flashing unlock这时手机屏幕会弹出解锁确认界面用音量键切换选择电源键确认。确认后Bootloader会执行解锁并清空数据整个操作几分钟内完成。再次检查Bootloader界面屏幕上会出现unlocked状态或者相关提示。解锁完成后设备的数据已经被清空了相当于一台全新的手机这是正常现象。之后如果要重新使用手机先开机跳过初始化再进开发者选项打开USB调试即可。5.3 刷入系统镜像flashall还是手动刷镜像目录已经就位现在进入最核心的刷入环节。最省事的方式是用编译系统自带的刷机脚本在out/target/product/sunfish目录下执行cd /path/to/aosp/out/target/product/sunfish fastboot flashall -w-w参数表示刷机的同时清空用户数据分区解决数据残留可能导致的开机异常。flashall脚本会自动识别当前目录下的所有镜像按顺序刷入整个过程包含boot、dtbo、vbmeta、super等分区非常省心。如果你更想理解每个分区的作用或者脚本出现了问题也可以手动逐个刷入。核心命令如下fastboot flash boot boot.img fastboot flash dtbo dtbo.img fastboot flash vbmeta vbmeta.img fastboot flash super super.img这里有几个细节。第一Pixel 4a是动态分区设备system、product、vendor都打包在super.img里所以不需要也不可能单独刷system.img这类分区除非你在烧录时切换到特殊模式。第二如果flash super时报invalid sparse file format或者大小不匹配通常是因为原super分区里还有旧系统残留。此时需要先清空super分区再刷fastboot wipe-super super_empty.imgsuper_empty.img就在编译输出目录里专门用于清空动态分区。这个操作会把super里的所有动态分区全部擦除然后再执行fastboot flash super super.img就顺畅了。这个坑我经历过当时反复刷了好几次都是报错清空后再刷一下就过去了。5.4 重启与验证看到自己编译的系统亮起来刷完后不需要手动断开电源直接执行fastboot reboot手机会重启第一次开机的时间会比正常启动长很多这是系统在初始化数据目录并进行首次启动优化。耐心等几分钟到十几分钟直到看到桌面或者引导界面。验证是不是自己编译的系统进入系统后打开设置-关于手机看构建版本号里是否包含你编译时的时间戳这是最直观的确认方式。也可以用命令行验证adb shell getprop ro.build.display.id adb shell getprop ro.build.version.sdk看到输出里出现自己编译的分支号说明整个流程已经闭环了。到这个阶段AOSP编译刷机的核心任务就算完成了。6. 实战翻车记录常见报错与排查思路这一章是从多次实操里攒下来的经验先给一个速查表再挑几个详细展开。报错现象大多数原因解决思路终端出现 Killed内存不足编译进程被系统杀掉增加swap或降低make并行数No space left on device磁盘空间不足清理out或ccache或换更大磁盘FAILED: ninja: build stopped具体错误需要向上翻日志查看真正的编译错误不是看最后一行repo sync过程中断网络连接不够稳定重新执行repo sync中断可续跑找不到构建目标aosp_sunfish分支里缺少设备相关仓库检查manifest或切换到完整分支flash super失败super分区有旧数据先fastboot wipe-super super_empty.imgfastboot devices无输出驱动或线材问题换线换口Linux下检查权限6.1 编译进行到一半被Killed这个现象在内存不足的机器上最为常见。编译工具链通常在链接阶段对内存需求最高多个子任务同时占满内存后系统会直接kill掉某些进程终端出现的是很简洁的Killed字样具体哪个进程被杀不一定看得到。解决办法是先看内存状况free -h如果确实紧张优先给系统增加swap文件。一个可行的做法是创建一个30GB左右的swap文件并启用过程并不复杂但能明显缓解链接阶段的内存压力。同时把make的并行数降低比如从make -j16降到make -j8减少同时运行的子任务数量。两个手段一起用大多数OOM问题都能解决。6.2 磁盘空间告急编译反复失败AOSP编译对磁盘空间的消耗是逐渐增加的out目录会随着构建推进越变越大。如果df -h看到根分区快满了最直接的办法是清理ccache缓存prebuilts/misc/linux-x86/ccache/ccache -Cccache -C会清空所有缓存立刻释放几十GB空间。如果还不够再考虑把整个out目录删掉重新编译rm -rf out但删掉out意味着之前的编译成果全部作废下次编译等于从头再来。所以我更建议编译之前就规划好磁盘分区最好把AOSP整个目录放在独立分区或独立挂载点上避免和其他数据抢空间。6.3 刷机后卡在开机动画卡开机动画的原因五花八门最常见的两个是分区残留和首次启动时间过长。如果是按键反馈正常、屏幕一直闪烁的动态logo先多等几分钟Pixel这类设备首次启动慢是正常的。如果超过十五分钟还在转可能就需要回到Bootloader重新刷入一次系统或者尝试在fastboot模式下先执行fastboot erase userdata fastboot erase cache清空用户数据和缓存后再重启。如果问题依旧最稳妥的兜底方案是下载厂商原厂镜像完整刷回确认系统没问题后再重新走一次编译刷机流程。6.4 解锁后OEM解锁选项消失有朋友遇到过解锁成功后手机的OEM解锁开关变成灰色甚至消失。这通常是解锁后系统校验状态还没更新导致的或者设备处于特殊状态。一般情况下进入系统完成初始化、联网更新一次状态后选项会恢复正常。如果长期不恢复先检查Bootloader界面是否真的显示unlocked如果已经解锁刷机其实不受影响不用纠结这个开关的状态。7. 一些值得记住的实战心得文章到这儿主要流程已经完整讲完。最后分享几条我这次实操下来最深的体会希望帮你少走弯路。第一开始之前先确认磁盘、内存和分支三者之间的匹配关系。磁盘空间不足可以补救但如果在只有8GB内存的机器上强行编译Android 13时间成本会非常高。建议条件有限的读者优先选择更老一点的分支比如Android 10或Android 11编译负担小很多流程却几乎一模一样。第二ccache一定要开。第一次编译也许体会不到它的价值但只要你还打算改动代码重新编译ccache的加速效果就是实实在在的。我后续做一个小修改整个编译时间从一小时多缩短到十分钟这就是ccache的意义。第三userdebug值得长期持有。用它调试比user版本方便太多几乎没有哪个刷机玩家会主动选择user版本除非你要模拟正式版环境。如果你需要看系统日志、拉Java堆栈、改系统文件userdebug的基本盘能力会让你省下不少额外操作。第四不要害怕刷砖。只要Bootloader还能进入、fastboot还能识别设备就说明没有走到最坏的情况。即便系统分区刷坏了也还有原厂镜像兜底。真正要害怕的是在电池几乎没电的情况下开始刷机——那是唯一可能造成不可逆问题的场景。最后再说一个小技巧每次编译前把out目录里的旧镜像或者重要配置复制到单独目录里。比如你这次编译的是AOSP下次想切换其他分支旧镜像还在恢复起来就很从容。整个流程跑通之后你拥有的不仅是一台运行着自己编译系统的手机更是对安卓系统构建链路的一次完整理解。从源码到真机的每一环都在你手里过了一遍这种掌控感是直接下载刷机包完全无法替代的。