1. 程式人生 > >安卓中的增量升級實現-SmartUpdate

安卓中的增量升級實現-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. 指紋相同則,增量升級成功,客戶端應用生成的新檔案.指紋不同則拋棄增量升級,改為普通全量升級.

專案地址