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年提出的算法,专门用来处理二进制文件的差异。它的核心思想是:

  1. 扫描旧文件,构建一个后缀数组(suffix array)或哈希表,用于快速匹配。
  2. 扫描新文件,在旧文件中找最长匹配子串。
  3. 输出差异:匹配的部分用“复制旧数据+少量修改”表示,不匹配的部分直接写入新数据。

举个例子:旧文件是“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的处理流程是这样的:

  1. 解包镜像:把镜像拆成多个独立文件(比如kernel、ramdisk、dtb等)。
  2. 逐文件对比:对每个子文件分别用bsdiff算法计算差异。
  3. 重新打包:把差异结果和元数据重新组合成差分包。

这样做的好处是:如果只改了ramdisk里的一个脚本,imgdiff只会记录ramdisk的差异,而不会把整个boot.img重新对比一遍。效率高很多。

注意:imgdiff要求新旧镜像的“分区布局”必须一致。如果旧镜像有3个分区,新镜像变成了4个,imgdiff会直接报错。我曾经因为调整了分区表,导致imgdiff生成失败,排查了半天才发现是分区数量对不上。

3.3 OTA包签名与校验机制

升级包做好了,怎么保证它没有被篡改?这就是签名和校验的活。

Android OTA包的签名机制,说白了就是一套“数字信封”流程:

  1. 制作方:用私钥对升级包的内容摘要(hash)进行签名,生成签名文件。
  2. 设备端:用公钥验证签名,确认升级包是官方发布的,且未被篡改。

具体到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     # 差分包内容

校验流程分三步:

  1. 解压验证:Recovery模式下的updater会先解压META-INF目录,读取CERT.RSA里的公钥。
  2. 摘要比对:用公钥解密CERT.SF,得到MANIFEST.MF的hash值,然后自己算一遍MANIFEST.MF的hash,比对是否一致。
  3. 文件校验:遍历升级包里的每个文件,计算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更靠谱。
  • 签名与校验:用私钥签名、公钥验证,确保升级包完整且未被篡改。密钥管理是重中之重。

最后,我画了一张图,把整个升级包制作的流程串起来,方便你理解:

OTA升级包制作流程 旧版本镜像 (如 system.img v1) 新版本镜像 (如 system.img v2) 选择升级策略 整包 vs 差分包 差异计算 bsdiff:通用二进制文件 imgdiff:Android镜像专用 生成差分包 / 整包 签名与校验(signapk + 公钥验证) 输出:update.zip

这张图从左到右展示了升级包制作的完整链路:输入新旧镜像 → 选择策略 → 调用bsdiff/imgdiff计算差异 → 打包 → 签名校验。每一步都有坑,每一步都需要你仔细对待。

最后说一句:OTA升级包制作,看似只是几个命令行的事,但背后涉及文件系统、算法、安全等多个领域。我建议你动手做一遍:拿两个版本的AOSP编译产物,手动跑一遍ota_from_target_files脚本,看看生成的差分包里到底有什么。实践出真知。

公众号:蓝海资料掘金营,微信deep3321