安卓中的增量升級實現-SmartUpdate
SmartUpdate-增量升級
增量升級意義
增量升級即將需要升級的檔案與新版檔案做差分對比,產生差分包patch,然後將只差分包patch下發給使用者在客戶端生成新版檔案.達到節省流量的效果.在移動開發流量至上時代,這種增量升級方法非常實用.
經過測試驗證, 增量的效果還是非常不錯的.
增量升級方法
增量升級需要新舊版本差分對比,產生差分包, 然後與老版檔案合併成新版.這個過程重要的是怎麼產生差分包?怎麼差分對比能夠產生體積更小的差分包?
目前相關工具有bsdiff, * Xdelta*, * .PTPatch*, 自從 Android 4.1 開始, Google Play 引入了應用程式的增量更新功能
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
二.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進行驗證:
生成 d.patch 差分包
對比大小,發現可節省50%+的流量
3.bspatch生成新的apk包
bspatch的命令格式為:
bspatch oldfile newfile patchfile
bspatch old.apk new_res.apk d.patch
生成最終新檔案new_res.apk
和新版的原檔案對比指紋, 驗證是否生成有問題:
指紋一樣, 說明增量升級成功~
封裝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;
}
流程總結
- 伺服器生成各個提供整理升級的patch檔案及新檔案指紋.
- 客戶端到伺服器查詢是否有更新.
- 有更新則,上傳自己的版本號,查詢下載相應的patch檔案.
- 客戶端用patch生成新檔案, 並生成指紋與伺服器端新檔案指紋對比.
- 指紋相同則,增量升級成功,客戶端應用生成的新檔案.指紋不同則拋棄增量升級,改為普通全量升級.