Android_增量更新(BSDiff)详解

7,309 阅读2分钟

Android 增量更新,是在应用市场中非常常见的一个功能,并且很多游戏等 App 也会采用增量更新的方式升级版本,可以说是常见而且成熟的一门技术。

什么是增量更新

增量更新是基于 BSDiff 这个差量更新算法,基于两个 apk 字节码的差异,在服务端生成 patch 包,然后客户端通过同样的算法,把已安装的 apk 与 patch 包结合生成更新后的apk进行安装,以此减小 app 版本升级时的下载时间,提高更新效率。

增量更新有什么好处?

对于当前市场上大部分app,apk 体积一般在 100M 左右,假设网速 4M/s,那么全量更新需要的时间就是半分钟。至于增量更新,所需要的patch包与新老版本 app 改变的范围有关,绝大部分时间都可以减小一半以上的下载体积。

bsdiff 算法的原理

bsdiff_1.png

既然 bsdiff 是差量更新,那么这个算法的核心就在于,找不同。

首先,bsdiff 会记录以前缀分组的各个字符串组的最后一个字符串在old中的开始位置,这样对old文件中所有子字符串形成一个字典

static void qsufsort(long *I, long *V, u_char *pold, long oldsize)
{
    long buckets[256];
    long i, h, len;

    for (i = 0;i < 256;i++) buckets[i] = 0;
    for (i = 0;i < oldsize;i++) buckets[pold[i]]++;
    for (i = 1;i < 256;i++) buckets[i] += buckets[i - 1];
    for (i = 255;i > 0;i--) buckets[i] = buckets[i - 1];
    buckets[0] = 0;

    for (i = 0;i < oldsize;i++) I[++buckets[pold[i]]] = i;
    I[0] = oldsize;
    for (i = 0;i < oldsize;i++) V[i] = buckets[pold[i]];
    V[oldsize] = 0;
    for (i = 1;i < 256;i++) if (buckets[i] == buckets[i - 1] + 1) I[buckets[i]] = -1;
    I[0] = -1;

    for (h = 1;I[0] != -(oldsize + 1);h += h) {
        len = 0;
        for (i = 0;i < oldsize + 1;) {
            if (I[i] < 0) {
                len -= I[i];
                i -= I[i];
            }
            else {
                if (len) I[i - len] = -len;
                len = V[I[i]] + 1 - i;
                split(I, V, i, len, h);
                i += len;
                len = 0;
            };
        };
        if (len) I[i - len] = -len;
    };

    for (i = 0;i < oldsize + 1;i++) I[V[i]] = i;
}

然后,对比old文件和new文件,产生diffstring和extra string

    while (scan < newsize) {
        oldscore = 0;

        // 先找到相同代码的长度
        for (scsc = scan += len;scan < newsize;scan++) {
            ...
        };

        if ((len != oldscore) || (scan == newsize)) {
            // 再找到不同代码的长度
            ...

            // 将存在差异的地方放到 db中
            // 将新增的地方放到 eb 中
            for (i = 0;i < lenf;i++)
                db[dblen + i] = pnew[lastscan + i] - pold[lastpos + i];
            for (i = 0;i < (scan - lenb) - (lastscan + lenf);i++)
                eb[eblen + i] = pnew[lastscan + lenf + i];

            // 计算长度
            dblen += lenf;
            eblen += (scan - lenb) - (lastscan + lenf);

            // 将差异存在的位置等信息,写入到 pfbz2 中
        };
    };

最后,将diffstring 和extra string 以及相应的控制字用zip压缩成一个patch包


    /* 将存在差异的数据写入 pf 中 */
    if ((pfbz2 = BZ2_bzWriteOpen(&bz2err, pf, 9, 0, 0)) == NULL)
        errx(1, "BZ2_bzWriteOpen, bz2err = %d", bz2err);
    BZ2_bzWrite(&bz2err, pfbz2, db, dblen);
    if (bz2err != BZ_OK)
        errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);
    BZ2_bzWriteClose(&bz2err, pfbz2, 0, NULL, NULL);
    if (bz2err != BZ_OK)
        errx(1, "BZ2_bzWriteClose, bz2err = %d", bz2err);

    ...

    /* 将新增加的额外数据写到 pf 中 */
    if ((pfbz2 = BZ2_bzWriteOpen(&bz2err, pf, 9, 0, 0)) == NULL)
        errx(1, "BZ2_bzWriteOpen, bz2err = %d", bz2err);
    BZ2_bzWrite(&bz2err, pfbz2, eb, eblen);
    if (bz2err != BZ_OK)
        errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);
    BZ2_bzWriteClose(&bz2err, pfbz2, 0, NULL, NULL);
    if (bz2err != BZ_OK)
        errx(1, "BZ2_bzWriteClose, bz2err = %d", bz2err);

    /* 将文件读写指针移到头部,去写header */
    if (fseek(pf, 0, SEEK_SET))
        err(1, "fseeko");
    if (fwrite(header, 32, 1, pf) != 1)
        err(1, "fwrite(%s)", argv[3]);
    if (fclose(pf))
        err(1, "fclose");

最后,pf指向的文件就是新生成的 patch 包。可以看到这个算法中,用到了大量的bzip2这个包,对 patch 的体积进行了压缩。

如何增量更新

一、下载 bsdiff 源文件

download.png

download2.png

由于官网目前下载链接无法正常打开,所以我在网上找了一份windows上编译完成的项目。下载后,是一个编译完成的c语言程序。在source中能看到 bsdiff.c bspatch.c 等源码。

二、测试增量更新

既然 bsdiff 是基于二进制的增量更新算法,那么这个算法适用与任何文件类型,包括但不限于 apk txt jpg 等。所以这次,我以 txt 格式文本做简单的测试。

先简单写一个 old.txt 文件,里头就随便写几句话

old_txt.png

保存成功后,我们再新建一个 new.txt

new_txt.png

这时候,我们就有新老两个版本的文档,就可以通过 bsdiff 打增量更新的 patch 包了

bsdiff_txt.png

如果我们在命令窗口输入 bsdiff 的时候,如果没有给出需要的文件名,它就会提示参数格式。所以我们按照提示的格式将新老两个txt文档的名称填入,更新包的名称就叫patch吧

patch_txt.png

当命令执行完后,我们就可以看到 patch 文件已经存在于文件夹中了。有了这个patch包,我们就可以通过 bspatch 将 old.txt 通过 patch 重新生成一个文档,这时候我们命名为 result.txt

bspatch.png

如果我们只输入 bspatch, 也是会提示参数示意

patch_txt2.png

当命令执行完成后,我们就可以看到 result.txt 已经存在于文件夹中

result_txt.png

打开它一看,就能看到和 new.txt 一摸一样的内容了。

三、增量更新实战

现在我们了解了增量更新是怎么玩的,结合实际的业务场景,简单描述就是服务端根据新老版本生成 patch 文件,客户端下载对应的 patch 文件后,与本地的 apk 一起生成最新版本的 apk,最后执行安装命令。

这个过程就这么简单,顶多涉及到部分 ndk 开发知识

app_1.png

除了 bspatch.c 以外,我们还需要引入 bzip2 的源码 cmake.png

native_lib.png

bspatch_util.png

通过 BsPatchUtils 这个工具类,我们就可以引用到 bspatch.c 中的方法了

main.png

获取本地当前安装的 apk 和下载完成的 patch 文件后,我们就可以调用 BsPatchUtils 这个工具类生成最新版本的 apk,然后和全量更新一样直接根据 apk 文件进行安装就可以了。

最后一点思考

增量更新只能应用于两个指定版本的 apk 中,在实际情况中却有大量的版本运行于线上,所以需要有不同的 patch 文件去对应多个版本,也就需要后台根据版本生成并配置很多的文件。

同时,bsdiff 是基于二进制进行对比生成的 patch 文件,因此当客户端安装的是破解版的 apk,就会导致无法更新,最好对本地 apk 文件、patch 文件、生成后的 apk 文件,最好都进行 md5 校验。

本文作者:自如大前端研发中心-李益