高通平台的镜像越来越大这几乎是所有做过量产项目的同行共同的痛。编译出来的userdata、system、vendor动辄好几GB烧录一台机器等半天OTA包传到服务器也要半天。很多人第一反应是上压缩但有一个比压缩更基础、几乎零成本的操作经常被忽略——把原始镜像拆成sparse格式。这活儿做对了烧录时间、文件体积、传输效率都能肉眼可见地改善。这里讲的“拆分sparse”不是拿工具把稀疏文件做一次转换那么简单。真正麻烦的是如何在编译流程里自动把产物转成sparse同时不破坏AVB校验、不改变分区大小、不让烧录工具识别出错。这篇文章把我在高通平台项目里折腾sparse镜像的经验完整梳理一遍从格式原理、镜像生成位置、手动转与编译期集成的两条路线再到验证方法和实际踩坑一次性讲透。1. 拆分sparse之前先弄清这个格式具体解决了什么问题很多人第一次接触sparse是在烧录日志里看到sparse这个单词或者是在解包别人的镜像文件时发现工具报Sparse image detected。但真要让你解释sparse到底是什么、为什么能省空间、它和raw镜像有什么本质区别不少人只能说出“稀疏镜像更小”这一层。这里把原理拆开讲明白。1.1 sparse镜像的本质它不改变文件系统内容先做一个实验在Linux下创建一个16GB的空白文件往里面随便写点数据再看看磁盘占用。你会发现ls -lh显示16GB但du -sh显示的占用空间可能只有几百KB。这就是“稀疏文件”——文件逻辑大小很大但实际分配的数据块很少没有被写过的区域在磁盘上并没有真实占用。Android的sparse image格式做的是同样的事它抓的是文件系统层。ext4文件系统刚make完里面大部分block是空的或者全零。raw格式的镜像会把整个分区大小完完整整铺开哪怕全是零也要占着真实空间。而sparse格式用一个头部描述“这个镜像原本多大、由哪些chunk组成”再针对数据块做粗略的归类和记录。比如连续的零块会被记录成skip或fill chunk根本不存放原始数据真正有内容的数据块才作为raw chunk存进去。所以sparse文件通常比raw要小得多但解开之后拿到的是和原来完全一致的原始镜像文件系统内容和元数据一点不变。关键点在于sparse不是一种文件系统它只是一个包装格式。ext4还是ext4只是外面多了一层“块稀疏化描述”。这就意味着你完全可以拿fastboot直接烧sparse也可以先解包成raw再烧——最终在设备上落盘的字节流是等价的。1.2 高通平台镜像是大头哪些分区值得转不是所有分区都有必要做sparse处理。有些分区本身就是纯数据拷贝转不转意义不大。在高通平台常见的镜像里分区收益差别很明显我按实际经验给一个参考分区/镜像典型内容稀疏转换收益备注system系统只读文件系统极高包含大量未使用blockvendor厂商库、HAL、配置极高大量零填充区域product产品附加应用高依赖Makefile是否预留空间userdata用户数据中很多项目清零处理收益不定odmODM定制分区较高类似vendorboot/vendor_bootkernel和ramdisk低镜像本身较小无大量空洞super逻辑分区容器特殊内部包含稀疏逻辑规则复杂高速obb、预置APK、壁纸等大文件多的产品system和vendor的稀疏空间收益尤其明显。我遇到过一个项目vendor镜像raw大小接近4GB转完sparse后只剩约1.6GB体积直接砍掉一半以上。因为文件系统在生成过程中会标记大量未分配extent这些区域在raw里是空洞或填充数据在sparse里天然被跳过。还有一个容易忽略的点高通平台的烧录工具和fastboot默认是支持sparse分发的。也就是说烧录端会把sparse的chunk逐块解析并写入设备。即便最终烧录总时长不完全等同于文件大小比例省掉的数据传输量和解析时间也是实打实的。2. 高通平台编译产物梳理这些镜像在编译流程里是怎么生成的要在编译期自动拆分sparse首要任务是把镜像生成的位置和调用链摸清楚。很多人在这一步翻车急着改脚本却不知道别扭的镜像到底是在哪个target、哪个函数里写出来的。2.1 分区镜像与super镜像的差异高通平台较新版本上Android构建系统默认用动态分区system、vendor、product、odm这些分区被装进一个超分区镜像super.img。编译产物里既能看到单独的system.img也能看到打包后的super.img。super.img本身是一个分区间容器它内部有自己的表头和metadata逻辑分区在super内按一定方式排列和存储。如果单独把super这个容器整体转成sparse格式确实能减小文件体积烧录器也会支持但如果项目里需要刷单个逻辑分区比如只刷vendor调试那你单独生成的vendor镜像也需要是sparse的。比较完整的方案是逻辑分区镜像各自生成sparsesuper也按规则生成可烧录的sparse。这里要注意一个细节高通平台在编译时会有两套并行的镜像路径。一套用于构建逻辑分区镜像它们随后被合入super另一套则直接生成物理分区的镜像例如xbl、abl、boot、vendor_boot等。前者走build_image函数后者大多数情况下由高通自己的配置脚本或mk文件直接拷贝生成。2.2 高通BSP里的镜像生成入口这里以AOSP构建系统为主线结合高通BSP的定制来描述。镜像生成入口大致分布在三个地方build_image函数这是Android通用的镜像构建函数定义在build/tools/releasetools/目录下的脚本里或者较新版本在build/make/tools/相关路径。system、vendor这些文件系统镜像从目录树构建时会调用它内部按传递的属性文件决定文件系统类型、分区大小、标签、AVB配置等。mk文件中的自定义target高通BSP会在自己的device目录下定义各种.mk里面包含$(INSTALLED_XXXIMAGE_TARGET)规则。很多物理分区镜像比如xbl、abl、aop等由这些规则直接拷贝out目录下预构建产物或者在构建过程中用专门的打包脚本处理。build_super_image用于把多个逻辑分区镜像合并成super镜像。这个函数会检查每个输入镜像的属性并用工具生成最终的super镜像。super镜像内部的结构是否支持稀疏取决于构建系统版本的实现细节。想找到源头最直接的办法是在out目录下加一句打印用$(warning $(image))这类方式在mk里输出变量值或者临时把build_image脚本里的函数前面加上echo重新跑一次编译看哪些镜像走了哪些路径。这是笨办法但对于定位乱象效果最好。2.3 哪些镜像可以拆、哪些不能动分清楚可以sparse的镜像和不能乱动的镜像能避免烧录时出现诡异问题。可以转sparse的所有走文件系统生成的镜像比如system.img、vendor.img、product.img、odm.img以及它们合入的super.img。因为文件系统镜像内部充满空洞转成sparse后依然能被烧录工具正常解析。不建议转的xbl、abl、aop、tz等高通早期启动镜像这些通常不是文件系统镜像而是固件输入数据。它们内部可能已经有自己的封装且fastboot烧录时对这类镜像的解析方式与sparse的chunk机制不一定兼容。实测某些项目上强行转sparse后开机速度正常但后续版本的固件升级工具会因为这个格式判断出错。boot和vendor_boot这类镜像包含kernel、ramdisk镜像大小一般在一二百MB内转换收益有限而且部分平台对boot镜像的校验和地址对齐有严格限制转不转请先确认烧录用例。还有一种是“已经被填充的镜像”。有的分区构建流程里为了OTA或签名会先预留大量空间比如把vendor做到5GB但实际内容只有2GB这种raw镜像通常已经是“物理上稀疏”的转成sparse后就变成逻辑紧凑的包装体积大幅下降是sparse转换的最大受益者。3. 拆分的两条路子手动转换和编译期自动集成这是全文的核心操作部分。手动转适合一次性的版本验证编译期集成适合作为一个长期机制沉淀进构建系统。两种方式本质都一样调用工具把raw镜像变成sparse。区别在于你怎么优雅地把它塞进构建流水线里。3.1 手动后处理img2simg批处理脚本先看手动做法。AOSP编译输出目录out/target/product/xxx/下有一堆镜像system.img、vendor.img、product.img等。用img2simg把它们依次转换#!/bin/bash # 转换raw镜像为sparse镜像的简易脚本 OUTIMG_DIRout/target/product/xxx TOOLS$ANDROID_HOST_OUT/bin/img2simg for img in system.img vendor.img product.img odm.img; do if [ -f $OUTIMG_DIR/$img ]; then echo converting $img $TOOLS $OUTIMG_DIR/$img $OUTIMG_DIR/$img.sparse fi done转换完成后把.sparse文件替换原名或者在烧录脚本里直接指向.sparse文件。img2simg的所有参数里最常用的是默认保留文件的sparse chunk头另外可以指定-z来把sparse文件再压一层gz压缩。用法很简单但有几个细节要注意img2simg输出文件大小和raw镜像大小不同别用ls -l做对比要用du看磁盘占用。如果不想保留raw原文件转换后记得确认新文件能被烧录工具识别再删原文件。建议保留raw镜像用于后续OTA或签名环节。手动转换只适合快验不适合量产。因为一旦改了产品配置镜像重新生成你得再手动跑一遍很容易遗漏某一个分区。3.2 编译期集成从build_image改造到自定义target编译期自动生成sparse镜像最稳的做法是找到每个镜像的生成规则在target执行末尾追加一次转换或者在生成镜像时直接让底层工具输出sparse。比较常见的方式有下面两种。方式一改build_image函数Android从早期版本开始镜像build_image支持一个关键属性extfs_sparse_flag。在文件系统构建过程中如果这个flag被设置为-smake_ext4fs在生成ext4镜像时就会直接输出sparse格式。查一下你的BoardConfig.mk里可能已经有类似这样的配置BOARD_SYSTEMIMAGE_PARTITION_SIZE : 3221225472 BOARD_VENDORIMAGE_PARTITION_SIZE : 1610612736在此基础上可以再加BOARD_SYSTEMIMAGE_EXTFS_OPTIONS : -s BOARD_VENDORIMAGE_EXTFS_OPTIONS : -s但不是所以版本的构建系统都支持这个选项尤其是高通平台定制过的mk里经常有自己一套参数透传方式。如果加完之后发现生成的镜像还是raw去build_image脚本里看一眼调用make_ext4fs或mke2fs的地方看参数有没有透传。方式二写一个独立的sparse target如果build_image的选项被堵住了或者你想让所有镜像统一都转一次可以在项目的device/qcom/xxx/下的某个.mk文件里定义一个依赖所有镜像的post process目标ifeq ($(BOARD_SUPPORTS_SPARSE_IMG),true) # 把所有目标镜像打包进这个变量 SPARSE_TARGET_IMAGES : system.img vendor.img product.img odm.img .PHONY: sparse-all-img sparse-all-img: $(INSTALLED_SYSTEMIMAGE_TARGET) $(INSTALLED_VENDORIMAGE_TARGET) echo generate sparse images for img in $(SPARSE_TARGET_IMAGES); do \ if [ -f $(TARGET_OUT_IMAGES)/$$img ]; then \ $(HOST_OUT_BIN)/img2simg $(TARGET_OUT_IMAGES)/$$img $(TARGET_OUT_IMAGES)/$$img.sparse || exit 1; \ mv $(TARGET_OUT_IMAGES)/$$img.sparse $(TARGET_OUT_IMAGES)/$$img; \ fi; \ done endif然后用make sparse-all-img调用或者在$(PRODUCT_OUT)目标的依赖里加上它。这样每次编译完镜像会自动执行一次所有镜像的sparse转换。原理上这两种方式都只是“包装一层转换器”差异在于和你构建系统的耦合程度。方式一深入到底层工具转换发生在镜像生成阶段干净高效方式二留了一个可裁剪的入口适合当你不想破坏原本镜像生成逻辑、只想在产物输出阶段统一换格式时使用。3.3 自动化的前提定位镜像生成镜像的make目标不管选哪种方式前提都是能找到正确的target名称和依赖。有一个非常实用的trick在终端里执行make systemimage-showcommands make vendorimage-showcommands可以打印出具体执行了哪些命令、用了什么脚本、在哪个目录下生成镜像。想看到INSTALLED_SYSTEMIMAGE_TARGET之类的变量最终指向哪个文件也可以用make print-vars或者编译时加--no-print-directory观察输出日志。我用这个办法定位过不少问题。曾经有一个项目vendor.img不是通过AOSP标准build_image函数生成的而是公司自己的密文打包脚本先做加密再生成镜像。这种情况下你在BoardConfig里加再多的extfs选项都没用因为真正的生成入口是那个自定义脚本你必须把sparse转换加在脚本最后一步。这也是为什么要用showcommands把整条命令看清楚的原因。4. 编译期集成的关键细节时机、版本兼容与AVB签名顺序这一部分是真正拉开普通操作和工程化操作差距的地方。把sparse转换加在错误的位置轻则烧录后无法开机重则签名校验失败查起来相当折腾。4.1 转换时机直接影响签名和哈希很多人理解sparse是“对镜像外层再包装”签名是“对镜像内容算哈希”所以觉得转换顺序无所谓。这是最大的误区。Android的AVBAndroid Verified Boot机制对镜像进行签名和哈希计算的时候会针对设备上实际落盘的字节流。如果你在最终签名之前先转sparse然后在签名时把sparse文件当作raw来算哈希那么烧录到设备后设备端解析时会先sparse解包得到raw内容再按raw内容计算哈希和你签名时算的哈希就不是同一个对象校验失败设备直接进入橙色状态或拒绝启动。反过来如果你在签名之后再做sparse转换只要sparse解包后的raw字节流和签名时的raw文件完全一致校验就能通过。因为签名算的是解开后的内容而不是sparse文件本身的字节。所以在集成sparse时操作顺序必须是生成raw文件系统镜像。对raw镜像签名、打vbmeta哈希。签名完成后再做sparse转换。如果用的是build_image内部的-s标志去生成sparseAVB签名工具会先做解包处理确保哈希和签名逻辑一致。但如果自己写了个独立的sparse target挂在所有镜像生成完毕之后必须确保签名发生在它之前。实测中我遇到过一个特例有个项目要求vendor分区单独签名后再sparse结果没有改vendor的签名顺序拿到设备上一直报vbmeta校验失败。把转换从签名后移到签名前问题立刻消失。4.2 转换工具版本的选择AOSP自带工具和高通工具链Android构建系统里自带的img2simg/simg2img在build-tools里在编出的host工具目录下。高通平台BSP通常不会替换这两个工具但有一种场景需要用高通自己的工具某些平台的高通开机镜像工具链会直接生成非标准sparse格式它虽然有sparse的特征但内部封了一层高通私有头。遇到这种情况时纯度标准很重要。先确认你转换的镜像到底是标准Android sparse还是高通私有封装。最简单的方法是用hexdump查看文件头标准sparse文件的magic number是0xED26FF3A如果文件头看到的是其他魔数比如0xEA1A41E之类的就说明这是高通私有容器此时不要强行用img2simg去转换否则会把原有容器结构破坏掉。判断方法编译完后对super.img做一次parse检查用simg2img尝试解包。如果解包出来的raw文件大小和预期分区大小一致说明它是标准sparse如果解包失败或报错则要看是不是还包了一层其他格式。4.3 分区大小和稀疏展开尺寸的匹配问题还有一个坑经常在量产阶段才暴露分区表里定义的分区大小和sparse镜像展开后的大小不一致。举例而言你在BoardConfig里把vendor分区定义为2GB但生成vendor raw镜像的过程因为某些原因被撑到了2.5GBraw镜像也就是2.5GB。此时img2simg转换时如果不对输出做任何限制它会把2.5GB的raw按实际数据块情况生成sparse展开后是2.5GB超出了分区表里的2GB烧录时fastboot会提示大小不匹配或者写入越界。解决方式是在生成raw镜像时就严格控制分区大小确保ext4文件系统不会超出partition size。有两种做法make_ext4fs或mke2fs时指定-lsize参数保证镜像正好等于或略小于分区大小。如果已经是raw镜像用resize2fs缩小到合法分区大小再交给img2simg转换。缩小的操作要小心只能在文件系统层做不能直接截断文件尾部。用resize2fs精准缩到合法大小再做sparse转换才能保证安全。5. 拆分效果的验证格式识别、空间收益与烧录实测转换完毕不是终点。最怕的是看着好像转成功了实际上生成了一个损坏的文件烧进去直接变砖。所以必须有一整套验证流程。这也是量产项目排期的最后一道保险。5.1 验证文件到底是什么格式第一步确认镜像真的是稀疏的。最直接的办法是检查文件头部魔数hexdump -n 4 system.img.sparse如果输出是edff 263a实际显示格式可能为3aff26ed取决于字节序那基本可以认定是标准sparse格式。想更严谨直接用simg2img解包simg2img system.img.sparse system.img.raw ls -lh system.img.raw system.img.sparse如果sparse文件比raw文件小很多且raw文件能被正常挂载或解析说明转换质量没问题。如果能正常解包但大小和预期不符就得回头检查分区大小或转换参数。也可以用file命令部分系统版本内置了sparse识别的magic规则file system.img.sparse但不要完全依赖file命令因为它对文件系统镜像的识别逻辑经常把raw ext4镜像也识别成data识别结果参考价值有限。5.2 比较稀疏前后的体积与空间收益做一个简单的体积对比表格可以直观判断转换有没有达到预期效果镜像raw大小sparse大小转换率备注system.img3.2GB1.1GB34%压缩明显vendor.img1.8GB0.9GB50%大量空洞product.img1.2GB0.7GB58%中等boot.img96MB89MB93%收益一般上表只是理论示例不同项目的实际收益差异很大。如果发现某个镜像转换率超过80%说明镜像本身内容很“满”没有留很多空洞sparse转换没有意义如果转换率只有30%甚至更低说明原始镜像里很大一部分是空洞或填充数据这样的镜像最适合用sparse。判断一个镜像值不值得拆可以在编译完raw镜像后先跑一次du和ls对比逻辑大小和物理占用。如果发现raw文件在磁盘上本身就占用很小比如ls显示3GB但du显示1.2GB说明大多数区域是零块转sparse会非常有效。5.3 烧录实测注意fastboot和工具兼容性文件格式验证通过后务必做一次真实烧录测试。不能只停留在“文件能解包”的层面因为fastboot执行转换和Sparse解析的行为会受具体实现影响。测试时用fastboot flash system system.img.sparse如果fastboot能正常识别并烧录说明这一环没问题。如果看到“Invalid sparse file format”或类似报错常见原因有三种sparse文件的chunk type使用了该版本fastboot不支持的类型。文件头的total blocks和实际block数不匹配。文件在传输过程中被修改CRC校验失败。一个特别容易被忽略的问题烧录工具的兼容性。有些厂商自研刷机工具和fastboot行为不完全一致它们对sparse解析支持得不好。量产前一定用项目实际使用的烧录工具做验证不然fastboot能烧、工厂工具不能烧就麻烦了。6. 避坑与调优实际项目中反复踩到的几个点sparse拆分本身不难难的是那些分散在各个细节里的坑。这一章把我在多个项目里摔过的跟头集中整理出来希望对后来者有些帮助。6.1 不要对已经稀疏的镜像再次做稀疏这个错误看起来低级但发生概率不低。尤其当编译脚本和手动脚本混用时你可能在build_image里已经加了-s生成了sparse然后后续还有一个冒名顶替的sparse target又跑了一次img2simg。img2simg把sparse作为输入时会把它当作raw的“逻辑文件”再解析一遍输出的文件可能变成一个“包含原sparse文件数据的更大的sparse文件”。烧录时fastboot先解析外层sparse得到的是内层sparse文件数据无法直接当分区内容使用通常会报错或者刷入完全错误的数据。判断镜像是否已sparse有一个经验方法用ls -lh和du -sh对比如果逻辑大小和物理占用差距极大且hexdump的magic是0xED26FF3A就说明已经稀疏过了。此时不要再加工。6.2 期望值和实际收益关于sparse的实际收益很多人会有两个极端预期。一个觉得它会像压缩率广告一样把镜像缩小到20%另一个觉得它没什么用反正烧录时间主要不在传输。实际上sparse的收益主要取决于两点文件系统里的空洞比例以及烧录工具解析速度。空洞比例高转换后体积小。解析速度快体现在fastboot烧录时read和write的耗时差距上。以我测过的一个项目为例vendor从raw的2.8GB转sparse后变成1.2GB工厂烧录的耗时从原来的单分区约40秒缩短到约18秒。这个收益非常直接。但如果是boot这个分区raw可能才48MB转完之后没有本质变化反而是多一次转换过程且可能引入额外风险。所以做规划时应优先处理大分区和高空洞度分区其他分区保持raw即可。6.3 从构建产物到OTA打包链路里的sparse一致性最后提醒一个经常被忽略的环节OTA包。OTA包的生成流程在较新版本Android上很多是基于super分区镜像或逻辑分区镜像进行的。如果构建产物的system.img已经是sparse后面做OTA差分的时候OTA工具会先把sparse解包成raw再比对这没有问题但如果你在OTA生成时误用了未解包的sparse文件分区块哈希完全对不上会导致OTA升级失败或整包校验不过。有一种情况我踩过坑项目的OTA包是公司内部服务器上的脚本生成的它读取的是out目录下的镜像作为输入。某一次我把vendor.img直接改成sparse替换了原文件结果OTA脚本拿着sparse文件去和手机上的vendor分区做二分比对算出大量差异差分包做得非常大实测升级耗时暴涨。排查了半天才发现是输入镜像格式变了而内部脚本并没有做好解包。所以定一个规矩**构建产物里的raw镜像和sparse镜像分开命名保存。**例如system.img保持rawsystem_sparse.img作为sparse副本或者反过来。不要让“同名文件在不同状态下格式会变”这件事成为不确定因素。在OTA脚本、烧录脚本、加密脚本三个入口全部对齐格式后再发布。另外关于分区块大小和alignment高通平台在烧录时对物理分区有一定的对齐要求img2simg转换时最好使用默认的4K对齐即可不要为了追求极致压缩去调整chunk大小否则在后续fastboot write时会容易出现偏移量不匹配的问题。就我个人的工程习惯而言每次改完构建脚本后都会打一个全量编译验证包把system、vendor、super三个最关键镜像都跑一遍simg2img解包再看fastboot烧录结果。这套流程看着繁琐但在规模化出货之前能挡掉绝大多数问题。做镜像格式化这类底层工作稳比快重要。先把一条链路打通到可复现再谈优化和自动化才是可持续的做法。
阅读完成 · 觉得有帮助?