46、增量更新优化:BSDiff算法、补丁生成、补丁合成、增量更新框架
增量更新,说白了就是「只传变化的部分」。
我早年做系统OTA时,一个完整ROM包动辄2GB+,用户下载体验极差。后来引入增量更新,包体直接缩到几十MB。嗯,这里面的核心技术,就是BSDiff算法。
BSDiff算法原理
BSDiff全称是Binary Suffix Diff,它专门用来对比两个二进制文件,找出差异。我习惯把它理解为「二进制版本的git diff」。
核心思路分三步:
- 构建后缀数组:对旧文件建立索引,方便快速匹配重复块
- 扫描差异区域:遍历新文件,用后缀数组找最长匹配
- 输出补丁格式:把「匹配+插入+删除」指令打包成补丁文件
为什么选BSDiff而不是简单的逐字节对比?
你想想看,APK或系统镜像里,很多结构是「整体移位」的——比如新增了一个so库,后面的资源文件地址全变了。逐字节对比会认为「全变了」,而BSDiff能识别出「只是位置偏移了,内容没变」。这个特性,在Android系统升级中极其重要。
关键指标:BSDiff生成的补丁大小,通常只有完整包的5%-15%。我见过最极端的案例,一个200MB的framework.jar,只改了3行代码,补丁只有12KB。
补丁生成流程
补丁生成,就是「旧文件 + 新文件 → 补丁文件」的过程。我一般在构建服务器上跑这个步骤。
命令行用法很简单:
# 生成补丁
bsdiff old_file new_file patch_file
# 参数说明
# old_file: 旧版本文件(如旧APK)
# new_file: 新版本文件(如新APK)
# patch_file: 输出的补丁文件
但实际项目中,有几个坑要注意:
- 文件对齐:BSDiff对未对齐的文件效率很低。我习惯在打包前,先用
zipalign对齐APK - 内存占用:BSDiff需要加载整个旧文件到内存。一个500MB的system.img,内存占用轻松上GB。服务器上还好,移动端就别想了
- 耗时问题:大文件对比可能耗时几分钟。我建议在CI/CD流水线里异步跑,别阻塞发布流程
我的经验:如果补丁生成时间超过5分钟,考虑拆分成多个小文件分别对比。比如把APK里的classes.dex、resources.arsc单独处理,最后合并补丁。
补丁合成流程
补丁合成,就是「旧文件 + 补丁文件 → 新文件」的过程。这一步通常在用户手机上执行。
代码示例:
// 补丁合成核心代码(C语言)
#include <bsdiff.h>
int apply_patch(const char* old_file,
const char* new_file,
const char* patch_file) {
// 打开旧文件
FILE* old_fp = fopen(old_file, "rb");
// 读取补丁
FILE* patch_fp = fopen(patch_file, "rb");
// 执行合成
int ret = bspatch(old_fp, new_fp, patch_fp);
// 关闭文件
fclose(old_fp);
fclose(patch_fp);
return ret;
}
合成时最怕什么?我告诉你——内存不足。
BSDiff合成需要同时持有旧文件、补丁文件和新文件三份数据。在低端机上,一个100MB的补丁操作,可能吃掉300MB+内存。我曾经在红米4A上遇到过OOM崩溃,后来加了分块处理才解决。
避坑指南:合成前一定要检查可用内存。如果低于补丁大小的3倍,建议走完整包下载兜底。我曾经因为没做这个检查,导致线上5%的用户合成失败。
增量更新框架设计
一个完整的增量更新框架,我习惯分成三层:
| 层级 | 职责 | 技术选型 |
|---|---|---|
| 策略层 | 决定走增量还是全量 | 版本号对比、补丁可用性检查 |
| 执行层 | 下载补丁、合成新文件 | BSDiff库、分块下载、断点续传 |
| 验证层 | 校验合成结果 | MD5/SHA256校验、签名验证 |
我画了一张流程图,帮你理解整体逻辑:
框架设计时,有几个关键点:
- 版本兼容性:补丁只能从特定旧版本合成到新版本。我习惯在补丁文件名里带上「from版本_to版本」,比如
patch_v1.2_to_v1.3.bspatch - 降级策略:如果补丁合成失败,自动回退到全量下载。别让用户卡在「正在更新」界面
- 后台合成:合成操作放在后台线程,别阻塞UI。我见过有些App直接在主线程跑bspatch,导致ANR
我的习惯:在合成前先做「预检查」——对比旧文件的MD5是否和服务端记录的一致。如果不一致,说明用户本地文件已被篡改,直接走全量下载。这一步能避免90%的合成失败问题。
性能优化要点
增量更新虽然省流量,但合成过程本身有性能开销。我总结了几条优化经验:
- 分块合成:大文件拆成1MB的块,逐块合成。这样内存占用可控,也方便做进度回调
- 使用mmap:用内存映射代替read/write,减少用户态和内核态的数据拷贝。我在合成500MB+文件时,mmap比普通IO快了40%
- CPU亲和性:合成操作绑定到大核上跑。BSDiff计算密集,跑在小核上太慢
- 缓存策略:如果用户频繁更新(比如内测版),把补丁文件缓存到本地,避免重复下载
曾经踩过的坑:有一次线上发布,补丁合成成功率只有85%。排查发现是部分用户的旧文件被系统裁剪过(比如删除了无用资源)。从那以后,我强制要求所有增量更新必须做「文件完整性预检」,不匹配就走全量。
增量更新不是银弹,但用好了,用户体验提升非常明显。我个人建议:优先对APK、资源包、配置文件做增量更新;对于so库和dex文件,如果改动频繁,考虑用插件化方案替代。
公众号:蓝海资料掘金营,微信deep3321