3、升级包制作:差分包与整包的概念、bsdiff与imgdiff算法原理、OTA包签名与校验机制
各位好,这一章我们来聊聊升级包制作。说实话,这是整个OTA开发里最核心、也最容易踩坑的部分。我自己刚入行那会儿,就因为在差分包和整包的选择上没想清楚,结果产线上一批设备升级失败,被老大叫去喝茶……嗯,从那以后,我对这块就特别上心。
今天的内容,我打算分三块来讲:先搞清楚差分包和整包到底是怎么回事,再深入bsdiff和imgdiff这两个算法的原理,最后说说OTA包的签名和校验机制。这三块串起来,就是一条完整的升级包制作链路。
3.1 差分包与整包:两种思路,两种场景
先问个问题:你给手机推送一个系统更新,是把整个系统镜像(比如2GB)全推下去,还是只推变化的部分?
答案很明显——大部分场景下,我们选后者。这就是差分包和整包的区别。
| 对比项 | 整包(Full OTA) | 差分包(Incremental OTA) |
|---|---|---|
| 包体大小 | 大(通常1~3GB) | 小(通常几十MB~几百MB) |
| 升级耗时 | 长(下载+写入) | 短(仅下载差异部分) |
| 依赖条件 | 无依赖,任意版本可升 | 必须基于特定旧版本 |
| 适用场景 | 首次刷机、跨大版本升级 | 小版本迭代、安全补丁 |
| 制作复杂度 | 低,直接打包镜像 | 高,需要对比新旧版本 |
我个人习惯是:大版本升级(比如Android 12升到13)用整包,小版本迭代(比如安全补丁)用差分包。你想想看,如果每次升级都推整包,用户的流量和等待时间都受不了。
核心要点:差分包的核心价值在于“只传变化的部分”。但代价是——你必须保证设备当前版本与制作差分包时的旧版本完全一致。否则,升级会失败。
我在项目中遇到过这样一个坑:某次我们发布了差分包,但部分用户因为之前手动刷过第三方ROM,导致系统版本号虽然一样,但实际文件内容不同。结果升级时直接报错,设备变砖。后来我们加了一个版本校验的强约束,才解决这个问题。
3.2 bsdiff与imgdiff算法原理
差分包怎么做出来的?说白了,就是对比新旧两个文件,找出差异,然后只打包差异部分。Android里主要用两个工具:bsdiff和imgdiff。
3.2.1 bsdiff:二进制差异算法
bsdiff是Colin Percival在2003年提出的算法,专门用来处理二进制文件的差异。它的核心思想是:
- 扫描旧文件,构建一个后缀数组(suffix array)或哈希表,用于快速匹配。
- 扫描新文件,在旧文件中找最长匹配子串。
- 输出差异:匹配的部分用“复制旧数据+少量修改”表示,不匹配的部分直接写入新数据。
举个例子:旧文件是“abcde”,新文件是“abfde”。bsdiff会输出:
- 复制“ab”(匹配)
- 写入“f”(新增)
- 复制“de”(匹配)
这样,差分包里只存了“f”这个字节和几个控制指令,体积小很多。
个人经验:bsdiff对二进制文件(比如.so库、APK)效果特别好。我曾经对比过一个20MB的so库,新旧版本只改了不到100KB,bsdiff生成的差分包只有300KB左右。压缩比惊人。
3.2.2 imgdiff:针对Android镜像的优化
imgdiff是Android团队在bsdiff基础上做的定制版。它专门处理Android的镜像文件(比如boot.img、system.img)。
为什么需要imgdiff?因为镜像文件内部有特殊的结构:
- 镜像头部有校验和、大小等元数据
- 镜像内部可能包含多个分区(比如boot.img里有kernel和ramdisk)
- 镜像文件通常经过稀疏(sparse)格式处理
imgdiff的处理流程是这样的:
- 解包镜像:把镜像拆成多个独立文件(比如kernel、ramdisk、dtb等)。
- 逐文件对比:对每个子文件分别用bsdiff算法计算差异。
- 重新打包:把差异结果和元数据重新组合成差分包。
这样做的好处是:如果只改了ramdisk里的一个脚本,imgdiff只会记录ramdisk的差异,而不会把整个boot.img重新对比一遍。效率高很多。
注意:imgdiff要求新旧镜像的“分区布局”必须一致。如果旧镜像有3个分区,新镜像变成了4个,imgdiff会直接报错。我曾经因为调整了分区表,导致imgdiff生成失败,排查了半天才发现是分区数量对不上。
3.3 OTA包签名与校验机制
升级包做好了,怎么保证它没有被篡改?这就是签名和校验的活。
Android OTA包的签名机制,说白了就是一套“数字信封”流程:
- 制作方:用私钥对升级包的内容摘要(hash)进行签名,生成签名文件。
- 设备端:用公钥验证签名,确认升级包是官方发布的,且未被篡改。
具体到OTA包,Android用的是signapk工具,签名算法通常是RSA或ECDSA。签名后的升级包结构是这样的:
update.zip
├── META-INF/
│ ├── MANIFEST.MF # 文件列表及hash
│ ├── CERT.SF # 签名文件(对MANIFEST.MF的签名)
│ └── CERT.RSA # 公钥证书
├── system/
│ ├── build.prop
│ └── ...
└── patch/
└── system.img.patch # 差分包内容
校验流程分三步:
- 解压验证:Recovery模式下的updater会先解压META-INF目录,读取CERT.RSA里的公钥。
- 摘要比对:用公钥解密CERT.SF,得到MANIFEST.MF的hash值,然后自己算一遍MANIFEST.MF的hash,比对是否一致。
- 文件校验:遍历升级包里的每个文件,计算hash,与MANIFEST.MF里记录的hash比对。
只要任何一步对不上,升级就会中止,并提示“签名验证失败”。
避坑指南:我曾经遇到过一个问题——升级包签名时用的私钥和设备里预置的公钥不匹配。原因是开发环境里有多套密钥,同事不小心用了测试密钥签名,而设备里烧录的是正式密钥。结果升级包在设备上永远验证失败。从那以后,我要求团队必须用统一的密钥管理工具,每次签名前检查密钥指纹。
另外,Android 7.0之后引入了APK Signature Scheme v2/v3,但OTA包目前主要还是用v1签名(JAR签名)。为什么?因为v2/v3签名会把签名信息嵌入到ZIP文件的中央目录里,而OTA包在Recovery模式下解压时,有些旧版本的ZIP库不支持这种格式。嗯,这里要注意兼容性。
3.4 本章小结
这一章我们聊了三个核心点:
- 整包 vs 差分包:整包简单但体积大,差分包高效但有版本依赖。选哪个,看场景。
- bsdiff vs imgdiff:bsdiff是通用二进制差异算法,imgdiff是Android镜像的专用优化版。处理镜像文件时,imgdiff更靠谱。
- 签名与校验:用私钥签名、公钥验证,确保升级包完整且未被篡改。密钥管理是重中之重。
最后,我画了一张图,把整个升级包制作的流程串起来,方便你理解:
这张图从左到右展示了升级包制作的完整链路:输入新旧镜像 → 选择策略 → 调用bsdiff/imgdiff计算差异 → 打包 → 签名校验。每一步都有坑,每一步都需要你仔细对待。
最后说一句:OTA升级包制作,看似只是几个命令行的事,但背后涉及文件系统、算法、安全等多个领域。我建议你动手做一遍:拿两个版本的AOSP编译产物,手动跑一遍ota_from_target_files脚本,看看生成的差分包里到底有什么。实践出真知。