背景

差异化更新可分为两种,一种是基于源文件的差异化更新,该种方式成功率高, 算法简单,常用于平台相关的差异更新,但在移动端保存巨大的源文件、下载更新文件整合后再编译的方式显然是不现实的; 另一种即为现在广泛使用的方法即对可执行文件的二进制更新方式,BSDiff就是后者。

作者

BSDiff是一种可执行文件的二进制差异构建和应用修补工具。

据资料记录,作者为Colin Percival,早在2003年就已经写好了这个工具。官网是Binary diff,不过看来已经下载不到资源,处于不维护的状态。

差量更新算法的核心思想

尽可能多的利用old文件中已有的内容,尽可能少的加入新的内容来构建new文件。通常的做法是对old文件和new文件做子字符串匹配或使用hash技术,提取公共部分,将new文件中剩余的部分打包成patch包,在Patch阶段中,用copying和insertion两个基本操作即可将old文件和patch包合成new文件。

BSDiff算法的改进

Insertion操作会引起大量的指针变动和修改,要记录这些值才能在Patch阶段给修改过的区域重新定位,由于这些指针控制字必须在BSDiff阶段加入patch包,产生的patch包会较大。BSDiff通过引入diff string的概念,大大减少了要记录的指针控制字的数目,从而使得patch包更小。具体原理在后续。

算法差异

1、第一种:二进制比对

关于两个文件之间的差异,我们很容易想到二进制对比,这里举个例子。

比如Linux有个cmp的命令:

增量更新 python 增量更新算法_源文件

这里我直接找的别人输出的文件

增量更新 python 增量更新算法_近似匹配_02

我们看文件大小,差异文件有110个字符,源文件才11和10个字符。

增量更新 python 增量更新算法_增量更新 python_03

可以看到源文件只有10和11个数字,最后对比后的结果(因为还要包含内容和位置)可不止这么多,文件大了一倍不止。

这个问题很好解决,使用经典动态规划算法——最长公共子序列算法。上述两个文件对比可以很轻易的找出最长公共子序列为123456789。这样,只要在更新差异文件中记录0和其对应的位置,并在旧文件中插入,文件仅包含差异内容的复制及需要插入位置的索引即可,可以极大的减少更新包的大小,做到我们需要的差异化更新能力。

如上方式已经可以使用,但仅可以在源文件的差异对比中,如果是可执行文件(编译后文件),往往一个小的改动编译后的文件也会因为指针变化导致所有后续代码都发声位置改变,这样这个算法跟cmp那个方法如出一辙了。

2、第二种:可执行文件二进制更新算法—BSDiff

为解决第一种问题,“差异更新界“专家们做出了很多努力,试图找出一些规律来避免这种可执行文件更新包过大的问题,如一个指针指向地址A更新后变为指向地址B,那么所有指向地址A的指针也会随之更新为指向地址B。在仔细挖掘可执行文件的内在规律后,确实有许多更新算法对可执行文件的更新文件压缩效率非常高。但大多高效算法都是与可执行文件的类型深度绑定的, 而Colin Percival在2003年即提出了一个优秀的算法BSDiff,可在平台无关的环境下做到极高的更新文件压缩效率。

BSDiff原理解析

可执行文件的更新会产生三类不同的文件变动:

1. 零阶变动:指编译过程中的固有变化,即完全相同的两段源代码在编译后也可能会发生变化。然而在现代大多数编译环境下,如Unix程序或Windows exe等,相同代码编译后并不会产生变化。

2. 一阶变动:直接修改源代码导致的变动,此变动会导致新旧文件大范围不一致。

3. 二阶变动:由于一阶变动间接引起的变动,每次插入或修改代码都会引起其他未修改代码部分的指针地址或寄存器地址变化,但该变动内的大部分其他二进制字段内容与旧文件保持相同。

在传统的差异更新算法中,要求新旧两文件的二进制的对比保持完全一致。而由于可执行文件中的二阶变动特点,完全一致的匹配方式会极大的增加更新包的大小。 类似ExeDiff等平台相关的更新算法可以将可执行文件反编译后找到可变部分并剥离出来,再进行其余指令的比对,将问题简化为源代码的比对问题。但在平台无关的可执行文件环境下,需要将问题转化为发现负责操作部分代码的二进制差异而非内存或寄存器信息的差异。

BSDiff算法的提出即针对可执行文件更新前后二阶变动的两个重要规律:1)没有被更新代码所影响的代码段,在变为可执行文件后,该区域的二进制内容的改变是极为稀疏的,即仅仅有部分指针或寄存器地址会变动,通常不会超过一两个字节;2)更新后的代码和数据会有很大的位置变动,但这种变动大多为整块的移动,即某一块位置中代码内的指针地址变动均会有相同的位移值。这两个规律导致一个重要的事实即:相同源代码对应的两个代码块中,大部分内容字节差为0,而少部分需要更新的地址位移数据又存在大量相同位移值,即源代码相同的代码块差异数据可以被高效压缩。

下面两张图是我百度来的,比较清晰的解释了差分过程。

增量更新 python 增量更新算法_源文件_04

增量更新 python 增量更新算法_增量更新 python_05

基于此思想,BSDiff算法使用如下步骤来进行生成差异更新包:

1. 将旧文件二进制使用后缀排序或哈希算法形成一个字符串索引。

2. 使用该字符串索引对比新文件,生成差异文件(difference file)和新增文件(extra file)。

3. 将差异文件和新增文件及必要的索引控制信息压缩为差异更新包。

首先是字符串索引的生成,该部分为差量更新算法的瓶颈部分,BSDiff算法里采用基于二分思想的Faster Suffix Sorting(更快的后缀排序)算法来进行索引的生成。后缀数组即一个一维数组,保存了i(1…n)的某个排列I[i],并且保证suffix(I[i])

增量更新 python 增量更新算法_可执行文件_06

该算法时间复杂度为O(nlogn),空间复杂度为O(n),其中n为旧文件的二进制字符串长度。

得到索引后,使用该索引依次查找新旧文件中完全匹配的最长二进制段,但并不会像传统更新算法一样直接打包,而是从该二进制段进行前后扩展,来生成范围更大的“近似匹配”,近似的要求是向前扩展的每个后缀及后向扩展的每个前缀至少有50%字节与旧字符串可以匹配(通常以8个不匹配字节作为阈值)。这些近似匹配可以认为是二阶变动导致的新代码,而非近似匹配的字段均可以认为是一阶变动新生成的字段。

在匹配完成后,更新包文件也即按此匹配方案生成,包含三个部分:

1)控制文件,包含需要添加和插入二进制段的指引信息(”添加指令”指定旧文件中的偏移量和长度,从旧文件读取适当的字节数,并将其添加到差异文件中的相同字节数;”插入指令”只是指定一个长度,指定的字节数是从额外的文件中读取的);

2)差异文件,包含近似匹配字段的字节差异;

3)新增文件,包含无法近似匹配的完全不同的字段。

这三个文件加一起会比新文件略大,但其中控制文件和差异文件是高度结构化的,意味着其均可被高效压缩,所以可以使用类似bzip2等压缩工具将更新包总文件进行非常有效的压缩。

总结

BSDiff的原理和技术非常成熟,可以用于apk的差分,测试了掌上炫舞和梦工厂已经自己测试的DemoApp,都可以完美合并。