为什么需要增量更新:节省流量,节省流量,节省流量,重要的事情说三遍!
增量更新不仅可以让用户在客户端实现省流量更新,更重要的是增量更新还可以实现服务器端流量的节省,为网站节省成本。
增量更新的原理服务端将应用的旧版本Apk与新版本Apk做差分处理,得到新版APK更新部分的差分包,例如旧版本的APK有6M,新版的有10M,更新的部分则可能只有4M左右(这4M文件除了包含更新内容以外,还包含一些上下文相关的东西),对于服务端来讲,如果是大型应用每次用户下载数量在百万以上,那么数百万*4M节省到的流量可想而知。这里强调一点,新apk和旧apk生成的差分包,其体积大小一定要小于新apk的体积,否则增量更新无意义。
客户端在用户下载了差分更新包之后,将手机端存在的旧版本软件APK(大多处在/data/data/app/下),复制到SD卡或者cache中,并与差分更新包进行合并,得到一个新版本的apk升级安装包,最终,这个生成的apk和你做差分之前的apk是一致的。
为了得到增量更新中的差分包,我们使用网上一款非常有名的开源二进制查分工具bsdiff。其中bsdiff依赖bzip2,所以我们还需要用到 bzip2工具。
关于bsdiff大家可以去官网下载http://www.daemonology.net/bsdiff/
关于bzip2大家可以去官网下载http://www.bzip.org/downloads.html
下载后的bsdiff目录如下
在bsdiff中,bsdiff.c 用于生成差分包,bspatch.c 用于合并文件。
ok,准备就绪下面开始步入正题,我将分为服务端和客户端两个方面给大家讲解。讲之前,还是先画个图,给大家从全局上描绘一下。
客户端开发流程讲解第1步.下载服务端的差分包
下面是通过自定义的下载方法,得到差分包文件patchFile 。
String downUrl = "http://www.castiel.com/apk.patch";
File patchFile = DownloadUtils.download(downUrl);
public static File download(String url){
File file = null;
InputStream is = null;
FileOutputStream os = null;
try {
file = new File(Environment.getExternalStorageDirectory(),Constants.PATCH_FILE);
if (file.exists()) {
file.delete();
}
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setDoInput(true);
is = conn.getInputStream();
os = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
} catch(Exception e){
e.printStackTrace();
}finally{
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
第2步.合并得到最新版本的APK文件
String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
String newfile = Constants.NEW_APK_PATH;
String patchfile = patchFile.getAbsolutePath();
BsPatch.patch(oldfile, newfile, patchfile);
这里的BsPatch是我们的定义的调用Native方法的类
public class BsPatch {
/**
* 合并包
* @param oldfile 旧版本文件
* @param newfile 新版本文件
* @param patchfile 合并后的文件
*/
public native static void patch(String oldfile, String newfile, String patchfile);
static{
System.loadLibrary("bspatch");
}
}
其中加载的baspatch.c文件
#if 0
__FBSDID("$FreeBSD: src/usr.bin/bsdiff/bspatch/bspatch.c,v 1.1 2005/08/06 01:59:06 cperciva Exp $");
#endif
#include <bzlib.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.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 main(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;
}
为了JNI开发方便,我们可以生成BsPatch的头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_castiel_inupdate_utils_BsPatch */
#ifndef _Included_com_castiel_inupdate_utils_BsPatch
#define _Included_com_castiel_inupdate_utils_BsPatch
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_castiel_inupdate_utils_BsPatch
* Method: patch
* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_castiel_inupdate_utils_BsPatch_patch
(JNIEnv *, jclass, jstring, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif
Android.mk文件配置
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= bspatch
LOCAL_SRC_FILES:= bspatch.c
include $(BUILD_SHARED_LIBRARY)
第3步.安装合并后的新APK
String newApkPath = Environment.getExternalStorageDirectory() + File.separator + "castielNew.apk";
installApk(MainActivity.this, newApkPath);
/**
* 安装Apk
* @param context
* @param apkPath
*/
public static void installApk(Context context, String apkPath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + apkPath),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
注意不要忘记添加权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
服务端开发流程讲解
其实开发非常简单,这里我简单的说明一下关键源码
public class BsDiff {
/**
* 导入差分库,调用差分方法
* @param oldfile
* @param newfile
* @param patchfile
*/
public native static void diff(String oldfile, String newfile, String patchfile);
static{
System.loadLibrary("bsdiff.so");
}
}
测试得到差分包
public class Test {
public static final String OLD_APK_PATH = "你服务器端的路径/castiel_old.apk";
public static final String NEW_APK_PATH = "你服务器端的路径/castiel_new.apk";
public static final String PATCH_PATH = "你服务器端的路径/castiel/apk/apk.patch";
public static void main(String[] args) {
//得到差分包
BsDiff.diff(ConstantsWin.OLD_APK_PATH, ConstantsWin.NEW_APK_PATH, ConstantsWin.PATCH_PATH);
}
}
增量更新的问题
- 增量更新在实施中,我们无法保证用户每次都能够及时升级到最新版本,所以必须对所发布的每一个版本都和最新的版本作差分处理,以便让所有版本的用户都可以进行差分升级,这样的流程除非通过自动化的脚本批量生成否则比较繁琐。
- 增量更新对于手机内存无法提供足够空间用作本地APK合成的用户和本地APK损坏的用户来说,是无法实现的。