1. 这个东西是什么,为什么会折腾到我
做Android系统编译的兄弟应该都有这种体会:代码同步下来、环境配好、lunch完、make一敲,接下来就是漫长的等待。但如果编的是Android R(也就是Android 11)这种带有动态分区(Dynamic Partition)的版本,那编译链路里会多出一个叫Super镜像的东西。它一旦报错,日志刷得飞快,但真正有用的信息往往藏在几百行输出中间,找起来能把人逼疯。
我这次遇到的就是Android R编译Super镜像时报错的问题。整个过程从最开始看到error关键字,到最终定位到根因并成功出包,花了整整一下午。事后复盘觉得这个排查路径挺典型的,也很值得记录下来——因为Super镜像报错不是高频问题,但一旦出现,涉及的知识点非常集中:lpmake分区大小计算、动态分区配置、super empty镜像生成、以及编译产物与设备端预期不匹配这几个环节。理解了这条链路,下次再遇到同类报错就能直接跳到可疑点,不用从头猜。
这篇文章适合谁看?负责Android系统编译、维护ROM构建脚本、或者在做动态分区方案定制的开发者。如果你只是偶尔编一次包,看明白排查思路也有帮助——至少下次报错时你知道该去翻哪个文件、看哪段日志,而不是对着终端发呆。
2. Super镜像在Android R里的角色与编译链路
2.1 动态分区机制下的Super镜像是什么
先花点篇幅把概念捋清楚。Android 10开始,Google在设备上引入了动态分区(Dynamic Partitions)机制,Super分区就是承载这套机制的物理分区。它不像以前那样把system、vendor、product直接烧写在独立的物理分区上,而是把这些分区作为“逻辑分区”塞进Super这个物理分区里。
这就好比以前一个小区里每栋楼都有自己的独立地基,现在改成一个大停车场,所有楼都停在这个停车场里,每栋楼之间用画线划分边界。这个停车场就是Super分区,画线就是分区表。好处是灵活性大幅提升——只要Super分区没写满,逻辑分区的大小可以动态调整,OTA升级时也不用因为某个分区差一点点空间就烧不进去。
Android R在动态分区上做了不少演进。具体到编译阶段,它会把system、vendor、product、system_ext等逻辑分区的内容打包,通过lpmake工具生成一个名为super.img的镜像文件。这个镜像会被刷写到设备的Super物理分区里。编译报错大多发生在lpmake执行这一环,因为lpmake对输入参数极其敏感——哪个逻辑分区的大小算错了、哪个分区顺序不对、或者Super分区总大小超过限制,都会直接中断。
2.2 编译Super镜像涉及的完整链路
从编译脚本的角度看,Super镜像的生成不是独立步骤,而是整个镜像构建流程的收尾环节。大致流程如下:
- 编译各分区镜像:system.img、vendor.img、product.img、system_ext.img等逐一生成。
- 读取动态分区配置:主要来自BoardConfig.mk里定义的BOARD_SUPER_PARTITION_SIZE、BOARD_SUPER_PARTITION_GROUPS等变量。
- 执行lpmake:把上一步编译好的镜像按配置文件的描述合并进Super镜像。
- 生成后续产物:super_empty.img、vbmeta相关的校验信息。
- 打包进完整刷机包。
这个链路里每一步都有对应的日志输出。我遇到的报错出现在第3步,错误信息指向lpmake的partition size计算问题。
2.3 报错现场记录
我当时的编译环境是某客户定制项目,基于Android R代码,修改了部分分区大小,把system分区调大以适应预装应用。改动本身不复杂,在BoardConfig.mk里调整了BOARD_SYSTEMIMAGE_PARTITION_SIZE的值,然后重新编译。
编译到后期阶段,终端刷出一段红色报错,核心信息如下:
[ 3% 53286/149865] build super image FAILED: out/target/product/xxx/obj/FAKE/super_xxx/.../super.img ... ERROR: lpmake: Not enough space on super partition.后面跟着一串super partition sizes、logical partition sizes的汇总表,一眼看过去system的大小明显超出预期。当时第一反应是“是不是system镜像真的变大了”,于是看了下system.img的实际大小,发现确实比之前增大了一些,但理论上不至于超。后来对比配置才发现,问题并不是出在“实际内容变大”,而是出在lpmake计算时的一个细节上。
3. 报错根因:分区大小与Super容量的账没算平
3.1 lpmake的容量计算方式
要理解这个报错,必须先搞懂lpmake是怎么检查空间的。lpmake拿到各逻辑分区的size后,会做两道计算:
第一道:把所有逻辑分区的大小累加,加上头部预留、metadata空间、alignment padding,得到一个所需总大小。
第二道:把这个总大小和BOARD_SUPER_PARTITION_SIZE(也就是Super物理分区的大小)比较。如果前者大于后者,就报Not enough space错误。
看起来很简单对吧?真正坑人的是“alignment padding”这一项。Android要求所有逻辑分区起点和大小都要对齐到一定单位,默认是1MiB。也就是说,即使你的system.img实际是1.5GiB,lpmake也会按1.5GiB向上取整到1.5GiB+某个余量来处理。当分区数量多、每块都有一点碎头时,累加出来的浪费空间相当可观。
我当时犯的错就在这。我基于system.img的实际文件大小去估算它在新配置下占用多少空间,却没考虑alignment。文件系统镜像大小和它最终在Super里占用的逻辑分区slot大小是两回事——前者是内容体积,后者是分配额度。在Android R里,逻辑分区大小由BOARD_SYSTEMIMAGE_PARTITION_SIZE或对应分区的size属性决定,这个值本就应该按对齐后的size来设。
3.2 我为什么会掉进这个坑
实际操作中,我修改system分区大小的方式其实是这样的:
原配置里BOARD_SYSTEMIMAGE_PARTITION_SIZE是一个固定的字节数,比如2GiB。我想把它增大到2.5GiB,就直接改成了2.5 * 1024 * 1024 * 1024。改完后system.img确实能编过,因为文件系统工具会按这个目标大小格式化镜像。但问题出在Super分组配置上。
在Android R的BoardConfig里,BOARD_SUPER_PARTITION_GROUPS这个变量会声明一个group(比如xxx_dynamic_partitions),group里包含system、vendor等分区的列表,同时group还有自己的大小限制BOARD_XXX_SIZE。如果group的总大小没有同步调大,那么就算Super物理分区本身够大,group内部的容量限制也会卡住system的新大小。
我当时只调了BOARD_SYSTEMIMAGE_PARTITION_SIZE,没动group大小,结果就是:system分区要2.5GiB,group里所有分区加起来的总需求超过group的容量上限。lpmake在构建时会先检查group维度,这层报错的日志不是最醒目的“Not enough space on super partition”,而是更隐蔽的一行“Requested size exceeds group limit”。
3.3 为什么这个问题有代表性
这个坑其实很有代表性。做系统定制的团队经常需要调整分区大小,但很多人只盯着单个分区的配置变量,忽略了动态分区体系里“分区-分组-物理分区”三层结构的联动关系。Single分区大小时,相当于只改了楼内面积,但停车场的总车位数和停车区域的划界没动,结果就是楼建出来了却放不进去。
Android R这套设计里还有一个容易忽略的细节:BOARD_SUPER_PARTITION_SIZE并不是设备上Super物理分区的最终大小。设备端烧录时,刷机工具可能会根据实际的GPT分区表重新调整Super分区。如果你在编译期配置的Super大小大于设备烧写脚本里定义的大小,还会引出一连串其他问题。这次虽然没踩到那个坑,但排查时我在代码里确认了一下相关配置,发现这些变量确实是互相牵制的。
4. 完整排查过程与定位思路
4.1 第一步:先别急着看代码,把报错日志完整捞出来
遇到编译报错,最忌讳的就是只看终端里最后几行红色信息。我见过不少同事直接对着ERROR那行开始改配置,改完重编,报错还在,然后继续改,陷入死循环。正确做法是把完整日志导出,找到lpmake执行的具体命令和完整输出。
这次我用的排查命令很简单:
cd $ANDROID_BUILD_TOP out/host/linux-x86/bin/lpmake \ --metadata-size 65536 \ --super-name super \ --metadata-slots 3 \ --device super:4294967296 \ --group main:3221225472 \ --partition system:readonly:2684354560:main \ --partition vendor:readonly:872415232:main \ ...手动执行lpmake的好处是可以直接看到它完整打印的每个参数和校验结果,不被make的进度条干扰。同时我用脚本对比了改动前后的两个配置文件:
diff <(cat device/xxx/BoardConfig.mk.bak) <(cat device/xxx/BoardConfig.mk)这个diff结果一目了然:我只改了BOARD_SYSTEMIMAGE_PARTITION_SIZE,BOARD_SUPER_PARTITION_GROUPS里对应的group大小还是原来的值。
4.2 第二步:核对三层容量的数字
这一步就是把账算清楚。以我的实际数字为例:
- Super物理分区大小:4GiB(4294967296字节)
- 动态分区group总大小限制:3GiB(3221225472字节)
- system逻辑分区:原配置2GiB,改为2.5GiB后变成2684354560字节
- vendor逻辑分区:约832MiB(872415232字节)
- product逻辑分区:约1GiB(1073741824字节)
把几个partition加起来加上metadata和alignment开销,原来2GiB的system时总量约3.8GiB,刚好卡在group的3GiB限制边缘,编译勉强能过。把system改成2.5GiB后,总量直奔4.3GiB,直接就爆了。
这里的计算有一个隐含点:group大小和super物理分区大小是两个上限,lpmake取的是更严格的那个。所以就算我把Super物理分区调大到6GiB,group不跟着动,照样报错。
4.3 第三步:确认是“真超”还是“假超”
排查到这一步,还要排除一种情况:配置上显示超了,但实际上.system.img自带稀疏文件(sparse image)特性,实际写入Super的数据可能小于逻辑分区大小。如果只在lpmake这层看问题,容易出现误判。
怎么确认?我当时的做法是直接看编译产物:
ls -lh out/target/product/xxx/system.img out/host/linux-x86/bin/img2simg out/target/product/xxx/system.img /tmp/system.simg ls -lh /tmp/system.simg注意,system.img本身就是sparse格式,img2simg只是做个验证。重点在于:即使system.img是稀疏的,lpmake分配逻辑分区slot时依然按非稀疏大小来算。也就是说,分区slot分配的是最大容量,不等于实际刷写数据量。这个特性是正常的,不代表lpmake计算有误。
所以判断“真超”的标准只有一个:所有逻辑分区slot需求总和是否超过group大小限制。我这边算完后,结论很明确——是真超了。
4.4 第四步:从历史改动中找线索
我还做了一件事:用git log检查BoardConfig相关的历史提交,看看这个项目之前有没有人调整过Super或group大小。这一步的作用是确认“当前设备烧写脚本里Super的实际大小”,避免我这边盲目把配置调大,结果超出设备GPT分区表的定义。
翻了一下历史记录,发现原始方案里group大小设置得很保守,只给了3GiB,而Super物理分区是4GiB。这个1GiB的差值本来可以作为扩展空间。查到这里,我心里就有底了:这次改动的正确姿势不是动Super物理分区,也不是动单个system分区,而是应该把group大小调上去,让它与物理分区容量匹配。
5. 解决方案:改哪里、怎么改、为什么这么改
5.1 最终改动方案
确定根因后,我做了两处配置调整:
第一处,把BOARD_SUPER_PARTITION_GROUPS里的group大小从3GiB调整到3.5GiB。之所以不直接调到4GiB,是因为需要保留一部分空间给metadata、alignment padding以及未来OTA分区的预留。Super物理分区的末尾还有一些非动态分区专用的空间,不能全塞给逻辑分区。
第二处,确认BOARD_SYSTEMIMAGE_PARTITION_SIZE的值符合对齐要求。原来的2.5GiB转化成字节是2684354560,这个数可以被1MiB整除,所以不需要再调。如果算出来不是对齐的,必须向上取整到1MiB的整数倍。
改动后的关键配置长这样:
BOARD_SUPER_PARTITION_SIZE := 4294967296 BOARD_SUPER_PARTITION_GROUPS := xxx_dynamic_partitions BOARD_XXX_DYNAMIC_PARTITIONS_PARTITION_LIST := system vendor product BOARD_XXX_DYNAMIC_PARTITIONS_SIZE := 3758096384注意BOARD_XXX_DYNAMIC_PARTITIONS_SIZE就是我用3.5GiB换算出来的字节数。3758096384除以1024三次等于3.5,这个数字看起来别扭,但Android的编译系统约定分区大小一律用字节,没得商量。
5.2 校验改动是否有效
改完配置后,我没有急着整个重编,而是先做了一个快速的增量验证。用到的命令是m精简目标:
make superimage只编Super镜像的好处是速度快,几分钟内就能验证lpmake是否通过。这个target会直接触发super.img的构建,不需要把system、vendor全量重编一遍(前提是这些镜像产物还在)。
编译通过后,我再看了眼生成的super.img体积:
ls -lh out/target/product/xxx/super.img看到文件正常生成,并且大小没有超过4GiB,我心里就踏实了。随后做了一次全量编译验证,确认没有引入其他连锁问题。
5.3 顺带补一个容易踩的坑:metadata slots
排查时我还注意到另一个配置项:BOARD_SUPER_PARTITION_METADATA_SLOTS。这个值在Android R里通常是3,它决定了Super分区里保存几份metadata副本。metadata并不大,但每一份都会占用Super空间。如果设备上实际定义的Super分区比较小,而metadata-slots设得很大,也会压缩可用的逻辑分区空间。
这块我这次没去动它,但建议大家在调整分区大小时把它纳入整体容量计算。简单说:metadata占了2%左右的空间,手动调group大小的时候别把这块忘了,否则实际可用空间会比你预期的略小。
6. 常见问题排查速查与实操总结
6.1 一张表搞懂Super镜像编译报错的常见原因
为了方便以后快速定位,我把Android R编译Super镜像常见的报错原因整理成了一张表:
| 报错特征 | 可能原因 | 快速验证方法 | 解决方向 |
|---|---|---|---|
| lpmake: Not enough space on super partition | Super物理分区或group总大小不足 | 计算所有逻辑分区slot大小之和 | 调大Super或group容量,或者缩小某个逻辑分区 |
| Requested size exceeds group limit | group容量配置偏小 | 对比BOARD_XXX_DYNAMIC_PARTITIONS_SIZE与各分区需求 | 同步调大group大小 |
| Invalid partition name | BoardConfig里分区名与lpmake参数不一致 | 检查BOARD_SUPER_PARTITION_GROUPS里的分区列表 | 统一分区命名 |
| Insufficient space in super partition for image | 某个逻辑分区实际镜像大小超过其slot大小 | 对比img大小与BOARD_XXX_PARTITION_SIZE | 调大该分区的size变量 |
| lpmake: invalid metadata size | metadata配置非法 | 检查BOARD_SUPER_PARTITION_METADATA_SLOTS和metadata大小 | 修正metadata配置 |
| Cannot find logical partition in group | 分区在group列表里缺失 | 检查PARTITION_LIST是否完整 | 把分区加进group列表 |
这张表不算完整,因为每家的定制情况不同,但覆盖了动态分区机制里最常出问题的几个维度。我遇到过不少编译报错,最后定位下来其实就是上表里某一行的场景。
6.2 实操中积累的几条心得
先说一个大家容易忽略的命令:make superimage。很多人一改分区配置就直接make全量包,其实完全没必要。Android构建系统支持单独构建Super镜像,利用这个target可以把验证周期从半小时压缩到几分钟。核心镜像(system、vendor、product)只要编过一次,改动BoardConfig后通常只需要重跑superimage就能确认配置是否合法。
再说如何正确计算super容量需求。我之前给过一个快速估算方法,这里再补充一点细节。假设你有n个逻辑分区,最坏情况下的总需求公式是:
总需求 = sum(各逻辑分区slot大小) + metadata大小 * metadata槽位数 + alignment padding * n各分区slot大小不是实际镜像文件的体积,而是你在BoardConfig里给它的size变量值。metadata大小一般是64KB起步。alignment padding每个分区最多1MiB(如果对齐单位是1MiB)。这个估算值如果比你配置的group大小大,那基本可以判定编译要挂。
第三个心得跟代码排查有关。改动BoardConfig后,建议先做一个小小的diff记录,把改动前后、以及你心里的预期三者对齐。很多时候报错反复出现,就是因为改了A变量忘了B变量。比如这次,我要是没做diff,可能还会去怀疑是不是磁盘空间不够或编译缓存坏了,白白浪费时间。
6.3 这个方案后续还能怎么扩展
Super镜像的排查经验不只是用在Android R上。Android S、Android T这些后续版本虽然动态分区机制有所演进,但lpmake的基本逻辑和“分区-分组-物理分区”三层结构一直保留着,排查思路可以平移到新版本上。
如果你的项目涉及OTA升级对分区表的动态调整,建议额外关注super_empty.img的作用。这个镜像携带了分区的默认布局信息,在设备初始化分区时被使用。如果编译期生成的分区布局与设备端预期不一致,刷机后会出现设备无法挂载system的情况,表现为启动卡在开机logo。这种情况比编译报错更隐蔽,我这次没踩到,但排查过程中查阅资料时留意到了,做个提醒。
7. 最后分享一个小细节
整个排查下来,我最深的体会是:Super镜像编译报错,问题往往不在“镜像”本身,而在镜像生成前的容量规划。Android R的动态分区把物理空间的管理从设备端移到了编译端,这也就意味着,每次改动分区大小的决策都必须放在“物理分区-动态分组-逻辑分区”三层结构里一起考量。单点修改一定会付出代价,只是早晚的问题。
如果这篇文章能帮你节省半小时的排查时间,那就值了。下次你的编译终端刷出lpmake报错时,不要慌,先把完整日志捞出来,算清楚三层的账,问题通常很快就能浮出水面。