IBigerBiger的成长之路

Android增量更新(一)增量更新

随着IT行业的蓬勃发展,现在国内的各种IT公司可谓是百花齐放,为了更快更好的占领市场,往往很多的App都面临着频繁更新的问题,而且一个App的大小基本上都是从几兆到几十兆不等,在有Wifi情况下,这种更新对于我们并没有太大的影响,但是在没有Wifi的条件下,更新App是非常耗流量,虽然现在运营商的网速是越来越快了,但是动辄几十的更新也确实会让用户有点Hold不住。

特别随着业务与功能的增多,App不断更新与升级,App体积也逐渐变大,不仅仅对于用户的更新带来比较麻烦的问题,同时对于服务器端的流量成本也是不断增大的,所以为了解决这个问题,Google在过往的IO大会上提出了增量升级,通过增量升级可以较好的解决上述的问题。

增量更新原理

增量更新的原理相对来说比较简单,服务端将应用的旧版本Apk与新版本Apk做差分处理,得到新版APK更新部分的差分包,例如旧版本的APK有10M,新版的有15M,更新的部分则可能只有5M左右(这5M文件除了包含更新内容以外,还包含一些上下文相关的东西),这里强调一点,新apk和旧apk生成的差分包,其体积大小一定要小于新apk的体积,否则增量更新无意义。

客户端在用户下载了差分更新包之后,将手机端存在的旧版本软件APK(大多处在/data/data/app/下)与差分更新包进行合并,得到一个新版本的apk升级安装包,最终,这个生成的apk与最初差分之前的apk是一致的,实际也就是上面说的新版本Apk。

所以其实流程就有如下关键的三点:

  • 利用old.apk和new.apk生成增量文件

  • 用户手机上提取当前安装应用的apk

  • 增量文件与当前应用Apk合并,生成new.apk,然后安装

接下来就是针对上述的问题进行说明

为了完成上述的功能,我们使用网上一款非常有名的开源二进制查分工具bsdiff。其中bsdiff依赖bzip2,所以我们还需要用到 bzip2工具。

关于bsdiff大家可以去官网下载http://www.daemonology.net/bsdiff/

关于bzip2大家可以去官网下载http://www.bzip.org/downloads.html

生成增量文件

关于增量文件的生成一般都是放在服务端去生成的,一般实际开发中会面临用户使用的版本与当前最新的版本插件会有不同的版本差距,所以必须对你所发布的每一个版本都和最新的版本作差分,以便使所有版本的用户都可以差分升级,可以通过用户的版本号来进行相关的差分,这个涉及服务器端的相关操作了,就不做深入介绍啦。

这里的增量是用到上述的两个工具

下载上述的bsdiff后,解压文件,到文件目录中,执行make


Snip20170330_1.png

我们会发现出现问题,当然这种错误怎么可能只有我们会犯呢,网上随便找一下就有相关的解决方案

OK在我们解决好上面的问题后,接下来就是介绍关于这个工具的使用了

首先准备两个Apk,ld.apk和new.apk,我们随便写个项目,先运行一次拿到生成的apk作为old.apk;然后修改些代码,或者加一些功能,再运行一次生成new.apk

  • 生成增量文件
1
./bsdiff old.apk new.apk PATCH.patch

通过这个命令就可以生成一个增量文件PATCH.patch

  • 增量文件和old.apk合并成新的apk
1
./bspatch old.apk newPatch.apk PATCH.patch

那么怎么证明这个生成的new2.apk和我们的new.apk一模一样呢?

我们可以查看下md5的值,如果两个文件md5值一致,那么这两个文件基本可以确定是一样的

如下:

1
2
3
4
iBoyandeMacBook-Pro:bsdiff-4.3 admin$ md5 new.apk
MD5 (new.apk) = 00266437bf16a229a7aa323fcc4837a8
iBoyandeMacBook-Pro:bsdiff-4.3 admin$ md5 newPatch.apk
MD5 (newPatch.apk) = 00266437bf16a229a7aa323fcc4837a8

可以看到两个文件的md5是一样的,所以两个Apk也是一模一样的了

增量文件已经生成好了,接下来就是客户端去做关于增量文件的合并了。

增量文件合并

其实前面已经提到了关于增量文件的合并了,但是怎么在客户端去实现对于增量文件的合并呢?

这就就要用到JNI相关的知识了,对于JNI的流程我这里就不做相关的介绍了

首先在build.gradle增加如下代码

1
2
3
4
5
defaultConfig {
ndk {
moduleName = 'patchUtil'
}
}

接下来在gradle.properties中增加如下代码

1
android.useDeprecatedNdk=true

完成这些配置后就可以开始代码的开发了

首先声明一个类,写一个native方法,如下:

1
2
3
4
5
6
7
8
9
public class PatchUtil {
static {
System.loadLibrary("PatchUtil");
}
public static native int patch(String oldApk, String newApk, String patch);
}

通过代码提示会自动创建一个PatchUtil的c文件,我们修改下为下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JNIEXPORT jint JNICALL Java_com_xinyuanhxy_increupdatedemo_PatchUtils_patch
(JNIEnv *env, jclass cls,
jstring old, jstring new, jstring patch){
int argc = 4;
char * argv[argc];
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 = patchMethod(argc, argv);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
return ret;
}

这个patchMethod是前面bsdiff解压后bspatch.c文件的main方法进行修改得到的,同时这里是需要我们上述的bzip2,将bzip2相关源码拷入jni文件下

还需要修改PatchUtil的include

1
#include "bzip2/bzlib.h"

经过相关的错误提示处理后就可以了,到这里就实现了关于JNI的编写了

接下来就是通过上面完成的JNI去做实际的应用了

可以通过下面的方法获取到当前应用的Apk

1
2
3
4
5
6
public static String extract(Context context) {
context = context.getApplicationContext();
ApplicationInfo applicationInfo = context.getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
return apkPath;
}

接下来看一下相关的调用方法

1
2
3
4
5
6
7
8
9
final File destApk = new File(Environment.getExternalStorageDirectory(), "newPatch.apk");
final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch");
PatchUtils.patch(FileUtils.extract(this),
destApk.getAbsolutePath(),
patch.getAbsolutePath());
if (destApk.exists())
FileUtils.install(this, destApk.getAbsolutePath());

这里通过我们事先放到文件根目录下的增量文件与获取当前应用的Apk,接下来通过JNI方法生成新的Apk文件,最后判断这个文件是否存在,存在则调用安装相关的方法如下:

1
2
3
4
5
6
7
8
public static 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());
}

运行效果如下


Untitled39.gif

到这里基本是完成了关于增量更新相关的知识了,当然在实际的应用中最好要对生成的Apk与最新Apk的md5值进行校验,确保Apk是否是可用的状态,虽然这里基本都是用的现成的工具,但是实际使用中还是会出现一些问题的,比如:

1.java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol “signal” referenced by “PatchUtil.so”…报错了

这个问题我在谷歌上面搜了下有解决方案如下:http://stackoverflow.com/questions/28740315/android-ndk-getting-java-lang-unsatisfiedlinkerror-dlopen-failed-cannot-loca

2.生成了新的Apk但是使用时候发现这个Apk是不可用的,解析错误

这个问题主要是安装版本,与我们生成Patch文件并非同一个Apk,所以要确保这两个Apk完全一致。

我们可以看到我们上面的动图中当生成新的Apk后会有很明显的安装过程,而这个这个安装是需要我们手动操作的,但是在日常使用其他的相关应用市场我们可以发现与上图的流程不太一样,所以下一篇会对于安卓中的静默安装进行相关讲解,peace~~~