一、前言
最近发现公司的APP版本升级有点频繁,而每次版本升级又都是整个APP全部下载后升级,如果是在流量下载的情况下,耗费流量不说,还影响升级时间,当我们个别APP比较大的时候,这种很差的体验就越发暴露明显,接着就被产品轻视,被用户吐槽,一系列的不满情绪,促使后来使用增量更新的方法去解决。说得增量更新,相信大家对bsdiff 和 bspatch这两个开源的工具库都不陌生,而且网上应该也有不少文章对此做了详细的分析,所以,今天这篇文章也主要是作为我个人对增量更新的理解和记录。
二、增量更新原理
好像网上每篇写增量更新文章,都会提及增量更新的原理。比如说将新旧两个版本的APK作差分,得到要更新部分的补丁(本地或服务器端完成),然后将补丁与用户已经安装的APK进行合拼得到新的APK(在APP内动态完成),进行重新安装。显然,这种说明也是正确的,不过我个人有个看似更形象的比如,那就是我们对本地代码和SVN代码进行对比的过程,把最新版本的代码通过如Beyond Compare这样的工具对比到本地,过程中代码不同的部分其实就相当于我们的补丁,本地代码合拼SVN新的代码后,重新运行应用,这个过程就是我们生产新APK的过程,所以,是不是两者感觉很类同?
增量更新涉及到两部分,一部分是APK作差分,生产补丁。它需要在我们的本地提前去生成,或者是写在我们的服务端,通过在本地上传新旧两款APK进行生成;二部分就是补丁和旧APK的合成与安装,在APK自身应用内完成,这个可以说是第一部分的逆过程。那么接下来就从这两部分来进行记录了解。
三、bsdiff制作过程
首先,我们可以从官网: http://www.daemonology.net/bsdiff/ ,下载bsdiff/bspatch的工具包。下载后是一个名字为bsdiff-4.3.tar.gz的包,里面只有两个c文件,一个bsdiff.c和bspatch.c,以及一个Makefile文件。就这么一个文件就能实现两个APK的差分?当然不是,打开Makefile文件发现里面还依赖了一个库bzip,所以还得下载这个依赖库,它的地址是:http://www.bzip.org/downloads.html 。 那么,把这些库下载完之后,如何进行链接起来进行编译,并在Java工程里面使用呢?因为补丁我们是在本地或服务器使用的,所以在这里我的做法是,把这些库使用Visual Studio软件生成一个dll库(Window平台),然后再使用eclipse新建一个普通的Java工程,并引入这个dll库,从而生成补丁文件。
接着,有了上面的思路之后,我们在vs新建一个c++工程,把bzip2库里面所有的.c或者.cpp的文件拷贝到源文件目录,.h的源文件拷贝到vs工程的头文件目录下,另外,再把bsdiff.c文件拷到源文件里面去。这时候,工程目录是这样的:
可能,大家会注意到上面的工程头文件目录多了三个.h的源文件,没错,因为我们dll库或者后面说到的so库的使用,要跟java上面的代码进行通信,我们使用的是Java提供的JNI接口,所以这个地方需要用到jni.h和jni_md.h头文件。可能你会问,这两个文件在什么地方,很简单,找到你jdk的安装目录,进入include目录就可以看到它们。OK,那对我们来说,如何去定义JNI的调用方法呢?做C语言开发的人应该都很了解,找main函数,没错,main函数就是入口。(注意:如果是打算通过windows命令行来生成补丁的话,跳过下面的步骤,可以直接执行编译生成dll文件,再配置到环境变量中去)
看到main函数的这行代码就可以了,这里argc表示后面argv数组参数的个数,第一个说明,可以随便填,第二、三、四个参数分别需要传的是 oldfile newfile patchfile ,即旧apk包的路径, 新apk包的路径 和补丁文件的路径。如此,我们只需要很简单的更改main函数为普通函数,然后再写一个JNI的方法来调用它就可以了。
1、main函数修改名字为bsdiff_main,其他代码不动
int bsdiff_main(int argc,char *argv[])
2、刚刚头文件那里还新增了一个 com_lsy_bsdiff_BsDiff.h的头文件就用到了,我们在bsdiff.cpp文件里面去实现它。新增方法:
JNIEXPORT void JNICALL Java_com_lsy_bsdiff_BsDiff_diff
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr){
int argc = 4;
char* oldfile = (char*)env->GetStringUTFChars(oldfile_jstr, NULL);
char* newfile = (char*)env->GetStringUTFChars(newfile_jstr, NULL);
char* patchfile = (char*)env->GetStringUTFChars(patchfile_jstr, NULL);
char *argv[4];
argv[0] = "bsdiff";
argv[1] = oldfile;
argv[2] = newfile;
argv[3] = patchfile;
bsdiff_main(argc,argv);
env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
env->ReleaseStringUTFChars(newfile_jstr, newfile);
env->ReleaseStringUTFChars(patchfile_jstr, patchfile);
}
package com.lsy.bspatch.utils;
public class BsPatch {
public native static void patch(String oldfile, String newfile, String patchfile);
static{
System.loadLibrary("bspatch");
}
}
命令javah生成头文件
这个JNI的方法是非常简单的,就只接受了三个参数,然后调用刚刚修改了的main函数方法bsdiff_main。运行该项目后,在工程debug目录对应的系统版本的文件夹下就可以看到Bsdiff.dll文件了。
3、使用eclipse新建一个Java工程,把Bsdiff.dll放在项目的根目录,随便新建一个类,如下:
执行该类,可以看到在D下就生成了diff.patch文件,到此补丁就生成了。
四、bspatch合拼补丁安装
如果说上面生成补丁不适用JNI的操作方法,而是在命令窗口完成即可, 那么,补丁在APK中的合成就不得不用到JNI的方法了,而且需要在Android的工程中使用。因为Android底层使用的是Linux,那么这个时候,我们需要把bspatch这部分编译成so库了。
首先,我们新建一个Android工程,创建jni目录,把bzip2目录和bspatch全部放进去,然后新建一个BsPatch类,和bsdiff基本一致。 右键项目Android Tools -> Add Native Support ,这样就会自动生成Android.mk文件。另外,bspatch.cpp文件里面的main函数修改方法名,然后增加JNI的方法和上面bsdiff的做法是一模一样的。
JNIEXPORT void JNICALL Java_com_lsy_bspatch_utils_BsPatch_patch
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr){
int argc = 4;
char* oldfile = (char*)(*env)->GetStringUTFChars(env,oldfile_jstr, NULL);
char* newfile = (char*)(*env)->GetStringUTFChars(env,newfile_jstr, NULL);
char* patchfile = (char*)(*env)->GetStringUTFChars(env,patchfile_jstr, NULL);
char *argv[4];
argv[0] = "bspatch";
argv[1] = oldfile;
argv[2] = newfile;
argv[3] = patchfile;
bspatch_main(argc,argv);
(*env)->ReleaseStringUTFChars(env,oldfile_jstr, oldfile);
(*env)->ReleaseStringUTFChars(env,newfile_jstr, newfile);
(*env)->ReleaseStringUTFChars(env,patchfile_jstr, patchfile);
然后,build project工程,就可以看到so生成了。
接下来就是调用这个JNI的方法了。
class ApkSysncTask extends AsyncTask<Void, Void, Boolean>{
@Override
protected Boolean doInBackground(Void... params) {
try {
//1.在这里从服务器下载我们的补丁文件,这里放本地替代
// ..........
//2、获取当前应用的apk文件/data/app/app
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), 0);
String oldfile = appInfo.sourceDir;
//3.合并得到最新版本的APK文件
BsPatch.patch(oldfile,newApkPath, patchPath);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if(result){
//4.安装合成后新的apk
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + newApkPath),
"application/vnd.android.package-archive");
startActivity(intent);
}
}
到这来,就可以看到apk的安装提示了。
五、结语
这篇文章对增量更新的介绍,其实是我们对NDK学习的一个很好的案例。 我也当时温故而知新了一回。