安卓中的增量升级实现-SmartUpdate

SmartUpdate-增量升级

增量升级意义

增量升级即将需要升级的文件与新版文件做差分对比,产生差分包patch,然后将只差分包patch下发给用户在客户端生成新版文件.达到节省流量的效果.在移动开发流量至上时代,这种增量升级方法非常实用.
经过测试验证, 增量的效果还是非常不错的.

增量升级方法

增量升级需要新旧版本差分对比,产生差分包, 然后与老版文件合并成新版.这个过程重要的是怎么产生差分包?怎么差分对比能够产生体积更小的差分包?
目前相关工具有bsdiff, * Xdelta*, * .PTPatch*, 自从 Android 4.1 开始, Google Play 引入了应用程序的增量更新功能,推荐使用bsdiff工具包。
bsdiff官网也说明了比另外两种工具更有优势, 所以此项目种也是用了bsdiff方式进行了封装.

>
bsdiff and bspatch are tools for building and applying patches
to binary files. By using suffix sorting (specifically,

Larsson and Sadakane’s qsufsort
) and taking advantage of how executable
files change, bsdiff routinely produces binary patches 50-80% smaller
than those produced by

项目说明

安卓项目中, 增量升级主要被应用与应用商店中的应用升级, 及rom的升级, 因为bsdiff是针对二进制文件的操作,固没有文件格式的限制,我们可以应用到插件的升级,数据库云端升级,资源升级等场景中.
增量升级的相关技术博客网上已经有很多, 且类似与此项目的封装也有不少, 但多少都有点实际问题,使得项目不用直接使用,固决定自己亲自实践一次, 记录下遇到的问题.

过程分析

一.需要的工具包及资源准备

  • bsdiff下载, 也可以从安卓源码中得到: \external\bsdiff
  • bzip2下载, pc上可以直接安装使用,方法:sudo apt-get install libbz2-dev .下载是用与封装成java代码共bsdiff依赖

二.PC上先验证下效果

1.解压bsdiff, 编译得到bsdiff/bspatch工具

这一步, 要确保bzip2在电脑上安装了
make编译得到bsdiff和bspatch工具, 但是通常make是build不成功的, 能力有效发现make调用的cc命令参数不对,但是又不知道怎么修改,只能手动编译

    gcc bsdiff.c -lbz2 -o bsdiff
    gcc bspatch.c -lbz2 -o bspatch

目录图

2.bsdiff工具生成patch差分包

bspatch的命令格式为:
bsdiff oldfile newfile patchfile

找两个不同版本的apk进行验证:
bsdiff

生成 d.patch 差分包
对比大小,发现可节省50%+的流量

old new patch

3.bspatch生成新的apk包

bspatch的命令格式为:
bspatch oldfile newfile patchfile

    bspatch old.apk new_res.apk d.patch

生成最终新文件new_res.apk

和新版的原文件对比指纹, 验证是否生成有问题:

md5

指纹一样, 说明增量升级成功~

封装bsdiff供android客户端使用

此安卓工程即是对bsdiff的封装,通过jni方式调用bsdiff的差分方法. 可以当成一个library 或是生成so库供其他项目直接使用.

封装bsdiff时也要引入依赖包 bzip2, 直接将下载的bzip2下的c文件放入jni目录下供调用.

编译时,由于bzip2种有很多main方法,应该是独立类测试或,独立使用用的, 导入项目中需要将bzip2中的main方法全部注释掉.
主要是根据bsdiff 源码
/* 此类主要参考bsdiff源码 */

    #include <stdio.h>
    #include "net_canking_smartupdatelib_SmartUpdateUtils.h"

    #include "bzip2/bzlib_private.h"
    #include <err.h>
    #include <unistd.h>
    #include <fcntl.h>

    static off_t offtin(u_char *buf) {
        off_t y;

        y = buf[7] & 0x7F;
        y = y * 256;
        y += buf[6];
        y = y * 256;
        y += buf[5];
        y = y * 256;
        y += buf[4];
        y = y * 256;
        y += buf[3];
        y = y * 256;
        y += buf[2];
        y = y * 256;
        y += buf[1];
        y = y * 256;
        y += buf[0];

        if (buf[7] & 0x80)
            y = -y;

        return y;
    }

    int applypatch(int argc, char *argv[]) {
        FILE *f, *cpf, *dpf, *epf;
        BZFILE *cpfbz2, *dpfbz2, *epfbz2;
        int cbz2err, dbz2err, ebz2err;
        int fd;
        ssize_t oldsize, newsize;
        ssize_t bzctrllen, bzdatalen;
        u_char header[32], buf[8];
        u_char *old, *new;
        off_t oldpos, newpos;
        off_t ctrl[3];
        off_t lenread;
        off_t i;

        if (argc != 4)
            errx(1, "usage: %s oldfile newfile patchfile\n", argv[0]);
        /* Open patch file */
        if ((f = fopen(argv[3], "r")) == NULL)
            err(1, "fopen(%s)", argv[3]);

        /*
         File format:
         0  8   "BSDIFF40"
         8  8   X
         16 8   Y
         24 8   sizeof(newfile)
         32 X   bzip2(control block)
         32+X   Y   bzip2(diff block)
         32+X+Y ??? bzip2(extra block)
         with control block a set of triples (x,y,z) meaning "add x bytes
         from oldfile to x bytes from the diff block; copy y bytes from the
         extra block; seek forwards in oldfile by z bytes".
         */

        /* Read header */
        if (fread(header, 1, 32, f) < 32) {
            if (feof(f))
                errx(1, "Corrupt patch\n");
            err(1, "fread(%s)", argv[3]);
        }

        /* Check for appropriate magic */
        if (memcmp(header, "BSDIFF40", 8) != 0)
            errx(1, "Corrupt patch\n");

        /* Read lengths from header */
        bzctrllen = offtin(header + 8);
        bzdatalen = offtin(header + 16);
        newsize = offtin(header + 24);
        if ((bzctrllen < 0) || (bzdatalen < 0) || (newsize < 0))
            errx(1, "Corrupt patch\n");

        /* Close patch file and re-open it via libbzip2 at the right places */
        if (fclose(f))
            err(1, "fclose(%s)", argv[3]);
        if ((cpf = fopen(argv[3], "r")) == NULL)
            err(1, "fopen(%s)", argv[3]);
        if (fseeko(cpf, 32, SEEK_SET))
            err(1, "fseeko(%s, %lld)", argv[3], (long long) 32);
        if ((cpfbz2 = BZ2_bzReadOpen(&cbz2err, cpf, 0, 0, NULL, 0)) == NULL)
            errx(1, "BZ2_bzReadOpen, bz2err = %d", cbz2err);
        if ((dpf = fopen(argv[3], "r")) == NULL)
            err(1, "fopen(%s)", argv[3]);
        if (fseeko(dpf, 32 + bzctrllen, SEEK_SET))
            err(1, "fseeko(%s, %lld)", argv[3], (long long) (32 + bzctrllen));
        if ((dpfbz2 = BZ2_bzReadOpen(&dbz2err, dpf, 0, 0, NULL, 0)) == NULL)
            errx(1, "BZ2_bzReadOpen, bz2err = %d", dbz2err);
        if ((epf = fopen(argv[3], "r")) == NULL)
            err(1, "fopen(%s)", argv[3]);
        if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET))
            err(1, "fseeko(%s, %lld)", argv[3],
                (long long) (32 + bzctrllen + bzdatalen));
        if ((epfbz2 = BZ2_bzReadOpen(&ebz2err, epf, 0, 0, NULL, 0)) == NULL)
            errx(1, "BZ2_bzReadOpen, bz2err = %d", ebz2err);

        if (((fd = open(argv[1], O_RDONLY, 0)) < 0)
            || ((oldsize = lseek(fd, 0, SEEK_END)) == -1)
            || ((old = malloc(oldsize + 1)) == NULL)
            || (lseek(fd, 0, SEEK_SET) != 0)
            || (read(fd, old, oldsize) != oldsize) || (close(fd) == -1))
            err(1, "%s", argv[1]);
        if ((new = malloc(newsize + 1)) == NULL)
            err(1, NULL);

        oldpos = 0;
        newpos = 0;
        while (newpos < newsize) {
            /* Read control data */
            for (i = 0; i <= 2; i++) {
                lenread = BZ2_bzRead(&cbz2err, cpfbz2, buf, 8);
                if ((lenread < 8)
                    || ((cbz2err != BZ_OK) && (cbz2err != BZ_STREAM_END)))
                    errx(1, "Corrupt patch\n");
                ctrl[i] = offtin(buf);
            };

            /* Sanity-check */
            if (newpos + ctrl[0] > newsize)
                errx(1, "Corrupt patch\n");

            /* Read diff string */
            lenread = BZ2_bzRead(&dbz2err, dpfbz2, new + newpos, ctrl[0]);
            if ((lenread < ctrl[0])
                || ((dbz2err != BZ_OK) && (dbz2err != BZ_STREAM_END)))
                errx(1, "Corrupt patch\n");

            /* Add old data to diff string */
            for (i = 0; i < ctrl[0]; i++)
                if ((oldpos + i >= 0) && (oldpos + i < oldsize))
                    new[newpos + i] += old[oldpos + i];

            /* Adjust pointers */
            newpos += ctrl[0];
            oldpos += ctrl[0];

            /* Sanity-check */
            if (newpos + ctrl[1] > newsize)
                errx(1, "Corrupt patch\n");

            /* Read extra string */
            lenread = BZ2_bzRead(&ebz2err, epfbz2, new + newpos, ctrl[1]);
            if ((lenread < ctrl[1])
                || ((ebz2err != BZ_OK) && (ebz2err != BZ_STREAM_END)))
                errx(1, "Corrupt patch\n");

            /* Adjust pointers */
            newpos += ctrl[1];
            oldpos += ctrl[2];
        };

        /* Clean up the bzip2 reads */
        BZ2_bzReadClose(&cbz2err, cpfbz2);
        BZ2_bzReadClose(&dbz2err, dpfbz2);
        BZ2_bzReadClose(&ebz2err, epfbz2);
        if (fclose(cpf) || fclose(dpf) || fclose(epf))
            err(1, "fclose(%s)", argv[3]);

        /* Write the new file */
        if (((fd = open(argv[2], O_CREAT | O_TRUNC | O_WRONLY, 0666)) < 0)
            || (write(fd, new, newsize) != newsize) || (close(fd) == -1))
            err(1, "%s", argv[2]);

        free(new);
        free(old);

        return 0;
    }

    /*
     * Class:     com_cundong_utils_PatchUtils
     * Method:    patch
     * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
     */
    JNIEXPORT jint JNICALL Java_net_canking_smartupdatelib_SmartUpdateUtils_applyPatch(JNIEnv *env,
                                                                                       jobject obj,
                                                                                       jstring old,
                                                                                       jstring new,
                                                                                       jstring patch) {

        char *ch[4];
        ch[0] = "bspatch";
        ch[1] = (char *) ((*env)->GetStringUTFChars(env, old, 0));
        ch[2] = (char *) ((*env)->GetStringUTFChars(env, new, 0));
        ch[3] = (char *) ((*env)->GetStringUTFChars(env, patch, 0));
        printf("ApkPatchLibrary old = %s ", ch[1]);
        printf("ApkPatchLibrary new = %s ", ch[2]);
        printf("ApkPatchLibrary patch = %s ", ch[3]);


        int ret = applypatch(4, ch);

        (*env)->ReleaseStringUTFChars(env, old, ch[1]);
        (*env)->ReleaseStringUTFChars(env, new, ch[2]);
        (*env)->ReleaseStringUTFChars(env, patch, ch[3]);

        return ret;
    }

流程总结

  1. 服务器生成各个提供整理升级的patch文件及新文件指纹.
  2. 客户端到服务器查询是否有更新.
  3. 有更新则,上传自己的版本号,查询下载相应的patch文件.
  4. 客户端用patch生成新文件, 并生成指纹与服务器端新文件指纹对比.
  5. 指纹相同则,增量升级成功,客户端应用生成的新文件.指纹不同则抛弃增量升级,改为普通全量升级.

项目地址

Github项目源码

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章