简介:Qt环境下的bsdiff增量补丁生成方案,面向需要在应用中实现轻量高效更新的开发工程师。这一名为differentialUpgrade的移植项目,将bsdiff的差异比较能力与qlz快速压缩算法结合,覆盖接口封装、数据流处理、性能优化与异常恢复等完整环节,有效解决了传统压缩方案速度慢、资源占用高的问题。资源包共705个文件,以idx索引文件为主,另有cpp/h源码、bin数据文件、pro/makefile构建配置及ui界面资源,整体约6.22MB,目录组织适合定位核心算法与测试数据。源码中包含新旧版本数据文件,可直接结合示例和文档运行验证,读者能从中学习如何将qlz压缩流封装为Qt风格类,并利用多线程提升整体patch生成效率。目前已有496人学习,适合对增量升级、文件差分同步或嵌入式更新方案感兴趣的开发者参考与二次开发。
1. 为什么 Qt 客户端做增量更新要自己移植 bsdiff
一个 Qt 桌面应用,每次发版都让用户重新下载几十上百 MB 的完整安装包,服务器带宽和用户更新时间都很难受。增量更新是常规解法,bsdiff 生成 patch 包是最常用的二进制差分方案之一。但原版 bsdiff 的压缩层绑死 bzip2,交叉编译费劲、回放解码慢,塞进 Qt 工程很别扭。把压缩算法换成 qlz(QuickLZ)之后,patch 生成和应用的耗时明显下降,内存峰值更好控,代价是 patch 通常比 bzip2 大一点,但客户端增量更新更看重速度和编译成本,这笔账划算。这篇笔记讲清楚把 bsdiff 移植到 Qt、用 qlz 生成 patch 包的全过程,适合做 Qt 静默升级、想自己生成补丁包的开发者。
2. bsdiff 与 qlz 的配合逻辑:patch 生成的流程拆解
2.1 bsdiff 到底在 diff 什么:控制块、diff 块、extra 块
bsdiff 不是逐字节比较。它先对旧文件做后缀排序,把旧文件的所有后缀按字典序排好,得到一个后缀数组;然后滑动扫描窗口,在新文件里逐段查找旧文件中最长的可匹配子串。匹配到的部分,如果两边内容接近但不等,就放进 diff 块,按字节记录差值;完全不匹配的大段新数据,直接放进 extra 块。控制块则是三元组序列,依次记录 diff 段长度、extra 段长度、以及旧文件读取位置的偏移量,回放时按这个序列把旧文件一步步改写成新文件。
举例来说,旧文件某处是0x90 0x91 0x92,新文件对应位置是0x90 0x91 0x93,diff 块只存0x00 0x00 0x01,几 KB 的数据在 diff 块里可能被压成几十字节。而对新增的几百 KB 资源段,extra 块直接存原始数据,控制块负责告诉回放端这一段要从 extra 块读多少字节。这个设计的价值在于:源码编译出的二进制新旧版本,大部分差异来自位移和个别字节变化,后缀排序能在旧文件里找到对应片段,diff 编码只存很小的差值,整体 patch 体积远小于完整新文件。
很多人会问:既然生成 patch 时新旧两个完整文件都在服务器端,为什么不直接把新包发下去?关键在分发差异。生成 patch 只需要在服务器做一次,回放 patch 却发生在每个用户本地,patch 的传输体积远小于新包,对低速网络用户,几 MB 的补丁和上百 MB 的整包完全是两种更新体验。bsdiff 的适用边界也很清楚:新旧版本同源、改动比例不高时效果最好;如果两个版本完全重写,diff 块和 extra 块加起来基本接近新文件本身,patch 就没有优势了。
由于 bsdiff 的处理结果是确定性的,同一对文件在哪个平台生成 patch,回放结果都应该一致,这给跨平台移植带来了一个方便前提。但要注意,patch 文件的字节级排布会随压缩层变化,生成端和回放端必须用同一套读写约定,否则就是自产自销的黑匣子。所以我一般先把自定义格式写死,再做移植。
提示:bsdiff 生成的 patch 只是 diff 数据的容器,压缩算法是独立的一层。原版把压缩做进流程里,所以很多人误以为必须配 bzip2,其实压缩层完全可以替换。
2.2 为什么原版默认 bzip2 不适合 Qt 场景
原版 bsdiff 写完控制块、diff 块、extra 块后,分别用 bzip2 压缩。bzip2 的压缩率确实高,但代价明确。第一,bzip2 在嵌入式或移动端交叉编译时要处理工具链匹配问题,Qt 工程本身可能跨 Windows、Linux、macOS,每个平台都要带一套预处理宏,维护成本高。第二,bzip2 解码慢,回放 patch 时用户要等好几秒,体感和整包下载差不多,增量更新的意义就弱了。第三,bzip2 按块压缩,解压时内存峰值容易冲到原始块的数倍,在内存受限的客户端设备上不友好。
qlz 的设计目标正好相反,它追求极快的压缩和解压速度、很小的状态内存、单文件实现。qlz 的压缩率低于 bzip2,但在增量更新里,diff 块本身就是差值信息,熵很低,再压缩的收益有限,qlz 的速度优势反而更珍贵。实际观察中,bzip2 压出的 patch 体积通常占优,生成和回放耗时则反过来,回放差距尤其明显。对分发环节来说,patch 大一点没关系,回放快、稳定才决定用户愿不愿意更新,所以从工程角度,qlz 替换 bzip2 是合理的取舍,而不是降级。
对比一下两个方案的特性,能更清楚这个选择:
| 维度 | bzip2 | qlz |
|---|---|---|
| 压缩率 | 高 | 中 |
| 解压速度 | 慢 | 快 |
| 交叉编译 | 需要独立库 | 单文件源码 |
| 状态内存 | 随块增长 | 固定可预估 |
| 适合场景 | 低频冷启动分发 | 客户端频繁增量更新 |
列完这张表,基本就知道怎么选了,关键决策点不是压缩率,而是回放速度和编译成本。
2.3 patch 格式设计与压缩层替换的边界
替换压缩层之前,先把 patch 文件格式定义写死。我用的布局:开头 8 字节 magic 标识这是 qlz 版 patch;接着三个 int64,分别是控制块长度、diff 块压缩后长度、新文件大小;然后是控制块数据、diff 块数据、extra 块数据。控制块本身不压缩,因为每段只有 24 字节,信息量小,压缩收益低;diff 块和 extra 块数据量大,走 qlz。为了回放端精确定位每块边界,我在每个压缩块前写一个 int32 的压缩后长度,回放端按长度读取和分配缓冲区,不会多读或截断。
具体字节布局上,控制块是连续的三元组序列,每组三个 int64,回放端每次读一组,根据 diff 长度和 extra 长度去对应块里取数据。因为控制块不压缩,读取时直接 fread 就行;diff 块和 extra 块前面各有一个 int32 压缩长度,读端先读长度,再读压缩数据,最后解压。这个布局的好处是 patch 文件可以流式读取,不需要一次性把整个 patch 加载进内存。
这个边界设计是整个移植最容易被忽略的地方。原版 bzip2 是流式压缩,回放端用BZ2_bzReadOpen按流读,天然知道压缩流在哪结束。qlz 是块压缩,一次压缩一个输入块,输出一个完整缓冲区,必须自己管理长度。如果只把BZ2_bzWrite换成qlz_compress,却不记录压缩后字节数,回放端根本不知道该读多少。我见过不少移植翻车案例,最后都出在这个长度字段,而不是压缩本身。所以生成端和回放端要约定死:每个压缩块前面总有 4 字节长度,读到长度 0 表示结束。把长度字段视为协议的一部分,后面就不会有玄学问题。
3. 在 Qt 工程里集成 bsdiff 并替换 bzip2 为 qlz
3.1 工程文件组织:哪些源文件要进 .pro / CMakeLists
移植第一步是决定哪些源文件进入 Qt 工程。常见做法是保留 bsdiff.c、bspatch.c 两个核心源文件,把 quicklz.c 和 quicklz.h 放进 vendor 目录,不引入额外依赖。qlz 的实现就是两个文件,编译极简单,这正是选它的原因。用 qmake 就等价在 .pro 里列源文件;用 CMake 就在 target_sources 里对应加。注意别把原版依赖的 bzlib.h 一起拷进工程,一旦混入,编译时会有一堆残留引用,后面还得清理。
一个可用的 qmake 工程配置:
TEMPLATE = app CONFIG += console c++17 CONFIG -= app_bundle SOURCES += \ main.cpp \ src/bsdiff.c \ src/bspatch.c \ vendor/quicklz/quicklz.c HEADERS += \ src/bsdiff.h \ src/bspatch.h \ vendor/quicklz/quicklz.h DEFINES += _FILE_OFFSET_BITS=64 QMAKE_CXXFLAGS_RELEASE += -O2逻辑说明:bsdiff.c 和 bspatch.c 都保留,后续做验证时两端都在手边。DEFINES 里_FILE_OFFSET_BITS=64让 off_t 在 32 位系统上也按 64 位处理,bsdiff 内部用 off_t 记录偏移,没有它,超大文件会直接截断。Release 加 -O2,bsdiff 的后缀排序非常吃优化,默认优化级别下生成慢得没法用。CONFIG += console 保证输出可见,排查问题时比 GUI 直观。
用 CMake 时,最稳的是把 quicklz.c 单独编成 OBJECT 库或静态库,C 和 C++ 的编译选项可能不同,分开编能避免警告被当成错误。项目里如果已经有 Qt 的 CMake 习惯,直接在 target_sources 里加文件即可,但要记得给所有涉及 bsdiff 的目标统一加_FILE_OFFSET_BITS=64。如果忘记,链接阶段可能不报错,但生成的 patch 对大文件是错的,这种错最难排查。
3.2 替换压缩层:从 bz2 到 qlz 的关键代码
bsdiff 原版的写文件集中在bsdiff_stream.write回调,原版用BZ2_bzWriteOpen、BZ2_bzWrite、BZ2_bzWriteClose写块。换成 qlz 时不要满文件改 bsdiff.c 内部的调用,而是自己实现一个 write 回调,在回调里完成 qlz 压缩并写文件。这样 bsdiff 的 diff 逻辑不动,bzip2 引用彻底隔离。
关键实现:
#include "quicklz.h" typedef struct { FILE *fp; qlz_state_compress *state; uint8_t *comp_buf; int comp_buf_size; } QlzWriteCtx; static int write_u32(FILE *fp, uint32_t v) { uint8_t b[4]; b[0] = v & 0xff; b[1] = (v >> 8) & 0xff; b[2] = (v >> 16) & 0xff; b[3] = (v >> 24) & 0xff; return fwrite(b, 1, 4, fp) == 4 ? 0 : -1; } static int qlz_write_cb(struct bsdiff_stream *stream, const void *buffer, int size) { QlzWriteCtx *ctx = (QlzWriteCtx *)stream->opaque; if (size <= 0) return 0; if (size > ctx->comp_buf_size) { ctx->comp_buf = (uint8_t *)realloc(ctx->comp_buf, size + 400); ctx->comp_buf_size = size + 400; } uint32_t csize = qlz_compress((const char *)buffer, (char *)ctx->comp_buf, size, ctx->state); if (write_u32(ctx->fp, csize) != 0) return -1; if (fwrite(ctx->comp_buf, 1, csize, ctx->fp) != csize) return -1; return 0; }逻辑说明:qlz_compress 要求输入缓冲区预留尾随空间,实现建议至少比原始数据多 400 字节,所以 comp_buf 按 size + 400 分配。每次压缩前先写一个 uint32 的压缩后长度,bspatch 端先读长度再读数据。write 回调的返回值遵循bsdiff_stream的约定,0 成功,-1 失败。参数说明:ctx->comp_buf 是复用的临时缓冲区,避免每块 malloc,减小内存碎片;ctx->state 通过qlz_size_of_state_compress分配一次,一个压缩状态能反复使用,不需要每个块重建。
回放端对应要做的是按长度读数据再解压,逻辑对称:
static int qlz_read_cb(struct bsdiff_stream *stream, void *buffer, int size) { QlzReadCtx *ctx = (QlzReadCtx *)stream->opaque; uint32_t comp_size = 0; if (fread(&comp_size, 4, 1, ctx->fp) != 1) return -1; if (comp_size == 0 || comp_size > ctx->comp_buf_size) return -1; if (fread(ctx->comp_buf, 1, comp_size, ctx->fp) != comp_size) return -1; qlz_decompress((const char *)ctx->comp_buf, (char *)buffer, ctx->state); return size; }逻辑说明:读端也先读 4 字节长度,再读压缩数据,最后解压到输出缓冲。comp_buf_size 校验很重要,防止坏 patch 文件导致长度溢出。参数说明:qlz_decompress的第三个参数是解压状态,由qlz_size_of_state_decompress分配,回放端同样可以反复使用。这段代码要放在 bspatch.c 的 stream.read 回调接入点。
这段替换同时涉及一个重要决定:bspatch.c 是否也要代码级保留。如果产品只在服务端生成 patch、客户端只做回放,bspatch.c 就是回放工具,必须留;如果客户端只更新不生成,bsdiff.c 可以编成独立命令行工具发布,不进入客户端安装包。我一般把两端都保留在同一个工程,通过编译参数开关决定打哪个,方便本地验证和自动化测试。这个结构对后续换压缩算法同样友好,所有和压缩有关的代码都集中在这两个回调文件里,其他 diff 逻辑完全不碰。
3.3 编译选项与大文件开关
bsdiff 原版对文件大小敏感,用 off_t 记录偏移,Windows 下不定义_FILE_OFFSET_BITS=64就是 32 位,超过 2GB 的文件偏移溢出,生成的 patch 直接损坏。这个宏必须在所有包含 bsdiff 头文件的编译单元一致,否则链接期可能出现一处 64 位、另一处 32 位的怪异行为。我把它放 .pro 的 DEFINES,同时放源文件顶部兜底。
#ifndef _FILE_OFFSET_BITS #define _FILE_OFFSET_BITS 64 #endif #include <cstdint> #include <cstdio>逻辑说明:放源文件顶部是防止某些构建系统没把 DEFINES 传全。另一个编译相关问题是栈大小,bsdiff 的递归后缀排序会在栈上分配临时结构,默认 1MB 栈在某些平台会崩。我在 Windows 下给 qmake 加QMAKE_LFLAGS += -Wl,--stack,8388608,Linux 下用ulimit -s 65536或改主线程栈大小。这个参数不显眼,但遇到生成 patch 崩溃又找不到原因时,优先级很高。
链接完可以快速验证一下 off_t 宽度:在 main 里临时输出sizeof(off_t),确认是 8。Windows 上如果没生效会输出 4。这个检查 10 秒就能做,能避免后面整套流程白跑。另外,bspatch.c 里也有一组读回调,替换时不要只改生成端,回放端必须同步改,避免生成端新格式、回放端旧格式各走各的。
4. 生成 patch 包的最小可运行流程与关键参数设定
4.1 用 QFile 读入新旧文件并调用 bsdiff
工程集成完成后,把 bsdiff 封装成 Qt 可直接调用的形式。我写一个 PatchGenerator 类,输入两个文件路径和输出 patch 路径。内部用 QFile 读文件,转成内存缓冲区后交给 bsdiff。注意不要用 QByteArray 反复 append 拼接,读文件前先 reserve 再读取,大文件下性能差距明显。
核心实现:
class PatchGenerator { public: bool generate(const QString &oldPath, const QString &newPath, const QString &patchPath); }; bool PatchGenerator::generate(const QString &oldPath, const QString &newPath, const QString &patchPath) { QFile oldFile(oldPath); QFile newFile(newPath); if (!oldFile.open(QIODevice::ReadOnly) || !newFile.open(QIODevice::ReadOnly)) { return false; } QByteArray oldData = oldFile.readAll(); QByteArray newData = newFile.readAll(); oldFile.close(); newFile.close(); FILE *fp = fopen(patchPath.toUtf8().constData(), "wb"); if (!fp) return false; QlzWriteCtx ctx; ctx.fp = fp; ctx.comp_buf = nullptr; ctx.comp_buf_size = 0; ctx.state = (qlz_state_compress *)malloc(qlz_size_of_state_compress()); bsdiff_stream stream; stream.malloc = malloc; stream.free = free; stream.opaque = &ctx; stream.write = qlz_write_cb; int ret = bsdiff((const uint8_t *)oldData.constData(), oldData.size(), (const uint8_t *)newData.constData(), newData.size(), &stream); fclose(fp); free(ctx.state); free(ctx.comp_buf); return ret == 0; }逻辑说明:bsdiff 需要整块内存做后缀排序,内存至少是旧文件大小的数倍,所以不做流式读取。bsdiff_stream的 malloc 和 free 回调用于内部临时分配,opaque 指向 QlzWriteCtx。参数说明:bsdiff 返回 0 表示成功,非 0 表示后缀排序或内存分配失败;fopen 用"wb"避免 Windows 把\n写成\r\n。
调用侧最简单是走 main 函数:
int main(int argc, char *argv[]) { if (argc < 4) { qWarning() << "usage: gen_patch old new patch"; return 1; } PatchGenerator gen; bool ok = gen.generate(argv[1], argv[2], argv[3]); return ok ? 0 : 1; }参数顺序固定,方便测试脚本调用。实际产品中我会加一个进度回调,因为 bsdiff 生成是 CPU 密集操作,在 Qt 主线程跑会卡界面。用 QtConcurrent::run 包一层,按阶段发信号:读文件、排序、diff、压缩写盘,每个阶段发一次,粗粒度但够用。不想动源码的另一个办法是把 bsdiff 编译成独立命令行工具用 QProcess 跑,能拿到退出码但拿不到阶段进度。
4.2 关键参数:扫描窗口、内存上限、qlz 压缩级别
bsdiff 源码里有几个控制匹配行为的常数。最常用的是匹配扫描窗口,默认 32 字节,决定搜索最长匹配时的向前覆盖范围。窗口越大,越容易找到长匹配,patch 越小,但生成时间涨。对源码编译的二进制,32 够用;对资源包或压缩过的数据,可以试 64 或 128,patch 更小但耗时可能翻倍。另一个相关选择是排序实现,qsort 版内存低但慢,sais 版快但吃内存,我一般选 sais 优先,大文件下差距明显。
qlz 压缩级别是另一组关键参数,1 到 3 级,1 最快最低压,3 最慢最高压。diff 块是差值数据,模式单一,用 3 级也不慢,patch 能再小几个百分点;extra 块是整段新数据,熵高,级别差异更明显。推荐参数起点:
| 参数 | 所在位置 | 建议值 | 说明 |
|---|---|---|---|
| 匹配扫描窗口 | bsdiff 匹配逻辑 | 32 | 资源包场景可调到 64 |
| 排序实现 | bsdiff 依赖的排序库 | sais 优先 | 大文件下速度差异明显 |
| qlz 压缩级别 | qlz_compress 调用 | 3 | extra 块收益明显 |
| 压缩状态内存 | qlz_size_of_state_compress | 固定分配一次 | 两端复用同一状态 |
内存评估有个简单公式:峰值约等于旧文件大小乘排序倍数,加上新旧两个文件本身。排序倍数取决于实现,sais 约 5 到 6 倍,qsort 约 2 到 3 倍但慢不少。如果目标设备内存 512MB,旧文件 50MB,sais 峰值可能接近 350MB,已经有点紧张,这时候优先考虑压缩旧文件或用更省内存的排序。qlz 的压缩状态本身只有几 KB,不值得为它省内存。生成 patch 的机器通常比回放机器性能好,但生成端内存不够时 patch 会直接失败,所以生成端要做一次内存压测,拿最大的历史版本跑一遍。
参数调整不要靠感觉。我一般固定新旧文件,用真实发布包跑三组:窗口 32 vs 64,qlz 1 vs 3,记录生成时间和 patch 大小。然后选一个生成时间可接受、patch 最小的组合写进配置。注意不同新旧版本的差异度下最优参数会变,所以每次版本发布前对照一下之前的参数记录。如果 patch 体积突然变大,先看是不是新增资源多,再决定要不要调窗口,不要盲目把 qlz 级别拉满。
5. 移植与生成 patch 的常见坑:现象、原因、处理
5.1 patch 生成成功,回放时 bspatch 段错误
现象:bsdiff 正常退出,patch 文件大小也正常,但回放工具一跑就段错误,或者输出文件损坏。原因通常是 qlz 写端的长度字段和读端不一致,要么是字节序问题,要么是块边界没对齐。write_u32 用手工小端序写入,而读端如果用 memcpy 直接读本地 int32,在大小端不同的平台上就会错位。
解决:长度字段统一用显式字节序读写函数,生成端和回放端共用同一个工具实现,不依赖本机字节序。同时检查回放端解压前分配的缓冲区,必须等于压缩前原始块大小,拿压缩后大小去分配会越界写。排查顺序建议二分定位:先写一个最小测试程序,直接调 qlz_decompress 读 patch 文件,看解压出的总长度是否等于新文件大小,不相等就是块边界问题,相等再回头看回放逻辑。
5.2 编译报错 undefined reference to BZ2_xxx
现象:工程加入 bsdiff.c 后,链接阶段报一堆BZ2_bzCompress、BZ2_bzWriteOpen未定义引用。原因:bsdiff.c 里仍然 include 了 bzlib.h,且代码里残留 bzip2 写文件分支,编译器把 bzip2 函数调用编进去了。
解决:全局搜索BZ2前缀,把 bzip2 调用连同条件编译宏删干净,用自定义 write 回调接管。想快速验证就先链接 bzip2 库,但那违背换 qlz 的初衷,建议一次性清理。我习惯在 .pro 里加注释"本工程不再使用 bzip2",防止后来维护的人又把旧代码拉回来。可以用grep -rn "BZ2" src/检查残留,确认输出为空再编译。
5.3 换 qlz 后 patch 变大很多,是不是选错了
现象:同一个新旧版本对,bzip2 版压出来 3MB,qlz 版压出来 8MB,感觉亏了。原因:qlz 本身压缩率就低于 bzip2,但如果变大超过两倍,问题通常出在压缩级别没调,或控制块也被压缩了。控制块重复度高但体积小,压缩收益低,还拖慢回放。
解决:先把 qlz 级别设为 3,确认只有 diff 块和 extra 块走压缩回调。同时接受现实:qlz 的 patch 就是比 bzip2 大,只要回放快、内存稳,这个代价值得。对 patch 大小特别敏感的渠道,我会用 qlz 生成后再套一层 zlib 二次压缩,而不是换回 bzip2。注意二次压缩只是优化,不是修复,先确认 diff 数据本身正确再加,否则会把问题掩盖在压缩层里。
5.4 大文件生成失败,提示内存不足或直接崩溃
现象:几十 MB 的文件对正常,一两百 MB 的新文件生成到一半就崩,或者报内存分配失败。原因:sais 排序需要旧文件大小 4 到 6 倍的额外内存,加上新旧文件缓冲区,200MB 的文件对峰值可能逼近 GB 级。生成端如果是 32 位编译,地址空间直接撑爆。
解决:优先把工程编译为 64 位,这是最省事的路;其次换 qsort 版或半递归版排序。还有一个隐藏点:QFile::readAll会在 QByteArray 里额外拷一份,改成用QFile::map映射文件返回 uchar 指针,或先 resize 再 read,能省一次复制。超过 1GB 的文件对不建议硬上 bsdiff,考虑分块差分或多个 patch 更实际。
5.5 大 patch 文件回放时数据错位,但小文件正常
现象:小文件对回放没问题,几十 MB 的 patch 回放后新文件后半段错位。原因:patch 头里的长度字段用了 int 读写,int64 被截断。bsdiff 内部很多长度是 int64,但我在自定义格式定义时偷懒用了 int32 存储控制块总长度,大 patch 下直接溢出。
解决:长度字段全部统一为 int64,宁可多占 4 字节,不要留截断隐患。检查所有读写长度的代码路径,包括 patch 头三个 int64 和每个压缩块前的 int32。压缩块前的 int32 因为块本身不会超过 4GB 可以保留,但控制块总长度必须 int64。这个坑会在用户量上来、版本包变大之后突然爆发,而且是偶现的,排查起来非常痛苦,所以在一开始就把类型钉死。
6. 用一个验证脚本确认 patch 可用性:回放、对比、测试用例
patch 生成只是前半程,确认能被回放才算完。我习惯生成后立刻做回放验证:把 patch 应用到旧文件,得到输出文件,再和原始新文件逐字节对比或对比哈希。这个验证要进自动化测试,不手动跑一次就完事。换过压缩层之后,生成端和回放端如果不共用长度约定,生成的 patch 很可能生成顺畅、回放即崩,只有回放能暴露这类问题。
验证逻辑可以写成 Qt 单元测试:
bool verifyPatch(const QString &oldPath, const QString &newPath, const QString &patchPath, const QString &appliedPath) { QStringList args; args << oldPath << newPath << patchPath << appliedPath; int ret = QProcess::execute("patch_apply_tool", args); if (ret != 0) return false; QCryptographicHash orig(QCryptographicHash::Sha256); QCryptographicHash applied(QCryptographicHash::Sha256); QFile newFile(newPath); QFile appliedFile(appliedPath); newFile.open(QIODevice::ReadOnly); appliedFile.open(QIODevice::ReadOnly); orig.addData(&newFile); applied.addData(&appliedFile); return orig.result() == applied.result(); }逻辑说明:patch_apply_tool 是工程里编译好的回放命令行工具,参数顺序是旧文件、新文件、patch、输出路径。用哈希比较替代整文件对比,速度更快,也能精确判断是否一致。参数说明:QProcess::execute 会阻塞等待子进程结束,适合测试场景;想并行验证多个 patch 可以换成 start 加信号槽。
验证通过后,我还会做两个额外检查:把新文件中间几兆字节故意改坏,重新生成 patch,确认回放会失败而不是静默产出坏文件;检查 patch 文件头 8 字节的 magic 是否符合预期,防止文件写了一半被意外中断。这两个用例加进回归后,之后换参数、改 qlz 级别、升级 Qt 版本,回归成本都很低。
我这几年最大的教训就是:自定义压缩层之后只信回放结果,生成端和回放端必须天天一起测,少测一次都可能放出无法更新的线上版本。希望帮到你。
本文还有配套的精品资源,点击获取