0. 前言
增量更新的原理还是比较简单的,但是真的实现起来遇到的坑简直让人吐血,我是在windows下实现的,各种各样的坑,各种各样的错误,折腾了整整两天多才完成了,因此记录下来。
先介绍下什么是增量更新和为什么我们需要增量更新,当我们发布新版本的时候,一些用户升级并不是很积极,反正我个人是不太喜欢更新手机上的APP,不知道大家是什么样的习惯,这就造成了新版本的升级率并不高。增量更新就是解决这个问题的途径之一。增量更新的实现原理是将下载下来的增量包与手机上的APK合并形成新的包,然后再次重新安装,相比下载整个安装包的做法大大减少了用户下载等待的时间。
1. 增量包的生成
首先第一步肯定是要生成增量包,所谓增量包就是对比并抽取出new.apk和old.apk不一样的地方形成的类似于补丁性质的文件。好在这个工作已经有bsdiff工具帮我们实现了,可以直接点击下载,里面也包含了下面要用到的bspatch工具。
将两个版本的apk文件置于目录下并执行bsdiff old.apk new.apk patch.patch命令即可生成如下图所示的patch.patch文件,这就是我们所需要的增量包。有了增量包,当然得和old.apk合体才能完成它的人生意义,增量包和old.apk生成new.apk可以使用bspatch工具实现,命令为bspatch old.apk new.apk patch.patch,这里就不具体演示了,如果想实验先把该文件夹下已有的new.apk移出去。有兴趣可以比对新生成的new.apk和原来new.apk的MD5值,肯定是一样的。
2. 获取本地apk
PC端实现增量包和old.apk的合体不能完成我们的需求,因为增量包经过网络被用户下载后是在手机端完成合体的,那么如何在手机端完成这个任务呢?
首先肯定是提取应用本地旧的apk,这比较简单,这里为了演示就不判空了,现实应用中如果检测不到old.apk(可能是被删除了)就没必要再给用户传输patch了,那么更新只能传完整包了。获取到old.apk路径的代码如下:
ApplicationInfo applicationInfo =getApplicationContext().getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
3. 增量包和old.apk合体
接下来就是在手机端将其和增量包合并。这个合并任务肯定会用到bsdiff以及bzip的源码,是需要native方法去做的,native方法的实现就是工具的源码,再编译成so包供Java使用。下面介绍一下详细的流程。
3.1 声明native方法
首先声明一个PatchUtil类,并声明一个native方法:
public class BsPatchUtil {
static {
System.loadLibrary("apkpatch");
}
public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}
执行Make Project进行编译,将会生成.class文件。.class的路径为app\build\intermediates\classes\debug\Java类路径,在main目录下执行javah命令即可生成jni目录下的.h文件备用。
javah -d jni -classpath [你的sdk路径]\platforms\android-22\android.jar;..\..\build\intermediates\classes\debug [你的包名+包含native方法的类(中间都用点隔开)]
3.2 导入源码
在实现BsPatchUtil之前,我们需要将bspatch.c以及bzip的相关源码拷贝到jni目录下,bzip的源码只保留.h头文件和.c文件,并将bspatch.c中的main()方法名修改为executePatch()。并且修改其中bzip的引入头为#include "bzip2/bzlib.h"
文件结构如下图所示:
3.3 实现bspatch_util.c
新建一个bspatch_util.c,把3.1中生成的.h文件include进去,下面源码的第9行就是在调用我们在bspatch.c中原来的main函数,只是被我们手动改名为executePatch()。
#include "com_example_patch_BsPatchUtil.h"
JNIEXPORT jint JNICALL Java_com_example_patch_BsPatchUtil_patch
(JNIEnv *env, jclass clazz, jstring old, jstring new, jstring patch){
int args=4; char *argv[args];
argv[0] = "bspatch";
argv[1] = (char*)((*env)->GetStringUTFChars(env, old, 0));
argv[2] = (char*)((*env)->GetStringUTFChars(env, new, 0));
argv[3] = (char*)((*env)->GetStringUTFChars(env, patch, 0));
int ret = executePatch(args, argv);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
return ret;
}
3.4 修改Gradle的配置
先在gradle.properties文件中添加NDK使用声明,android.useDeprecatedNdk=true。
接下来在build.gradle中添加NDK配置,如下图所示,
其中apkpatch就是我们命名的so库,如果想只生成指定CPU类型的so文件,可以进行如下配置。
ndk{
moduleName "apkpatch"
abiFilters "armeabi", "armeabi-v7a","x86"
}
3.5 生成so库
进行Rebuild Project操作,不出意外会遇到Multiple define of main错误,这个解决起来也比较容易,直接把报错的c文件中对应的main函数注释即可。重新编译即可生成so库:
3.5 调用so库
我们在BsPatchUtil中静态代码块中的System.loadLibrary("apkpatch")就是对so库的加载。
接下来就是Java逻辑了,这里我们方便演示直接把old.apk和patch文件置于SD卡中,直接把两者合体成新apk就达到我们的目的了,真正应用中是需要提取本地apk的。下面代码也比较简单,就是单纯的调用native方法,生成新的apk之后关闭自身并重新安装。
final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk");
final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");
String str = Environment.getExternalStorageDirectory()+"/"+"old.apk";
BsPatchUtil.patch(str, destApk.getAbsolutePath(), patch.getAbsolutePath());
if (destApk.exists())
install(this, destApk.getAbsolutePath());
private void install(Context context, String apkPath) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setDataAndType(Uri.fromFile(new File(apkPath)), "application/vnd.android.package-archive");
context.startActivity(i);
android.os.Process.killProcess(android.os.Process.myPid());
}
3.6 结果演示
点击按钮进行合体操作,关闭当前应用并重新安装新apk的应用。可以看到按钮上的字改成了“Hello New Word”,算是完成了增量更新的操作。
4. 关于增量更新需要注意的地方
(1)增量更新最大的优点就是不需要用户下载新的完整安装包,因为实在是太大了。但是合体之前一定要注意old.apk是否存在,也要对下载下来的差分包做合法性验证,防止中间人攻击。
(2)增量更新和热修复完全不是一个东西。区别是增量更新需要重新安装apk,而热修复不需要。
(3)最后说一下增量更新的缺点,不仅客户端需要维护关于增量更新的逻辑,服务器端更是如此。每次发布新版本,服务端都需要为以前所有的老版本生成对应的差分包,维护会变得复杂,好在可以用脚本去做。
5. 期间遇到的问题
做个貌似简单的增量更新其实想一次做到还是比较难的,期间也遇到了挺多坑,在Android开发——增量更新实战中遇到的问题中总结了,其中cannot locate symbol"signal" referenced by "libbsdiff.so"错误大概花了两天的时间才得以解决,如果有同学需要可以参考,避免被坑。