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进行验证:
生成 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生成新文件, 并生成指纹与服务器端新文件指纹对比.
- 指纹相同则,增量升级成功,客户端应用生成的新文件.指纹不同则抛弃增量升级,改为普通全量升级.