一、增量更新
1.概述
增量更新,是指通过分析出新apk与旧apk之间的增量文件,然后提取老版的app安装文件,并与增量文件合成新的安装包,之后重新安装即可。
增量更新可以让我们不必加载整个安装包,即可实现app的更新,节省了不少流量,提供更佳的用户体验。有些应用市场,就是通过此方式实现流量的节省。
2.实现步骤
1)利用新apk与旧apk生成增量文件;
2)提取手机上的旧apk;
3)合成旧apk与增量文件,安装合成的新apk。
接下来一步步来实现上述的步骤。
二、实现
1.生成增量文件
这个我们使用工具bsdiff.exe即可生成差异文件,这个,我已经忘了之前从哪里找到的windows上的exe文件,直接提供下载地址:windows下bsdiff与bspatch下载地址
当然也有linux版本的,需要自己下载,make,下载地址:http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz。
使用方法也很简单:
在所在目录中,cmd命令生成增量文件:bsdiff old.apk new.apk new.patch
2.提取手机上的旧apk
上面的都是在pc上完成,下面的提取与合成都放在app上完成。
提取旧apk,直接提供一个工具方法:
/**
* 提取本应用的apk路径
*/
public static String extract(Context context) {
context = context.getApplicationContext();
ApplicationInfo applicationInfo = context.getApplicationInfo();
return applicationInfo.sourceDir;
}
3.合成与安装
合成需要使用到Native方法,因此要打包一个so库文件,也方便以后使用,这里也涉及到ndk与jni的使用。下面我一步步实现。
1)下载与配置NDK
首先,要下载NDK,一般在AS中打开SDKManager在SDKTools中下载。但是这里不要这么做,在实践中,发现sdk中更新的sdk如果过高,在打出的so库中使用的一些方法会找不到,因为有些手机使用的库较早。我在刚刚接触这个功能时,sdk是r14,但是打包出的so库在荣耀7和魅族上都会抛出找不到指定方法的异常。抠脚一天,才在网上找到可能是NDK版本过高的原因,因此这里我实验通过的版本是r10e,下载地址:http://dl.google.com/android/ndk/android-ndk-r10e-windows-x86_64.exe。下载之后,安装到sdk目录中。
首先在gradle.properties中配置
然后是local.properties
好了,配置就差不多了。可能大家在用的时候还是会遇到一些配置问题,请自行百度NDK配置,我就配了这俩地方。
2)实现合成nativie方法与打包
a)合成的native方法如下:
package com.example.davidchen.patchdemo.util;
/**
* 增量更新,合成
* Created by DavidChen on 2017/1/4.
*/
public class BsPatch {
static {
System.loadLibrary("bsdiff");
}
public static native int bspatch(String oldApk, String newApk, String patch);
}
b)先make project,之后在指定的目录...\app\build\intermediates\classes\debug\com\example\davidchen\patchdemo\util下找到编译出来的class文件,再用javah -jni com.example.davidchen.patchdemo.util.BsPatch命令生成对应的头文件。文件位置在...\app\build\intermediates\classes\debug目录下。
接下来就是c实现部分
c)首先建立jni目录:
d)将之前的头文件剪切到该目录中,再建立c实现文件(这里是Patch.c)。当然还有核心的实现方法。这里还有个依赖的bzip,要从网上下载,网址:http://www.bzip.org/downloads.html。当然这个库里还有些要去掉的多余的文件与方法。这里提供一个精简过的包以及上面的c实现:native方法c实现。但是不要直接用我上面生成的文件,因为native方法的命名是由要求的,直接使用会出问题,但是代码可以拷过去,注意改一下方法名和include即可。
e)配置ndk生成对应不同cpu的abi架构的so
说一下,一般我们为了缩减apk大小,会选择最少的so,怎样去适配不同手机的cpu架构呢,一般的机子,大部分都是支持armeabi或armeabi-v7a,而且x86的机子也会支持armeabi的类库。因此我们一般只需要armeabi的so。
之后可以在...\app\build\intermediates\ndk\debug\lib目录中找到生成的so,拷贝到项目...\app\libs\目录下(当然也可以不拷贝,因为有c实现的时候可以打包后直接就加载到c的方法,但是如果不要c实现就要这些so了,下面会介绍)。
3)实现合成并调用方法安装
这里写了个在activity中运行的合成例子:
...
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
String path = Environment.getExternalStorageDirectory().getPath();
doBsPatch(this, path + "/new.apk", path + "/new.patch");
}
/**
* 合成
*
* @param latestPath 要生成的新apk位置
* @param patchPatch 增量文件位置
*/
public static void doBsPatch(final Context context, String latestPath, String patchPatch) {
final File latestApk = new File(latestPath);
final File patch = new File(patchPatch);
//一定要检查文件都存在
if (!patch.exists()) {
Toast.makeText(context, "合成失败", Toast.LENGTH_SHORT).show();
}
new Thread(new Runnable() {
@Override
public void run() {
// 使用native方法,是因为c的算法比java的算法要快100倍
BsPatch.bspatch(ApkExtract.extract(context),
latestApk.getAbsolutePath(),
patch.getAbsolutePath());
if (latestApk.exists()) {
ApkExtract.install(context, latestApk.getAbsolutePath());
}
}
}).start();
}...
上面的install方法实现:
/**
* 安装apk
*
* @param apkPath apk所在目录
*/
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());
}
注意声明读写权限。OK,测试图就不上了。
注意:so文件可以直接用来放到libs中对应的abi架构目录中使用,这样以后就不用再麻烦的用那么多的c文件和生成步骤了,只要在build.gradle中的android内添加:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
指定jni加载的so库位置,便可以直接通过java的native方法进行使用。三、应用
增量更新的应用还有些要注意的。
一般步骤是:
pc:生成新版本的apk-->与旧版本的apk生成增量文件。
app:检测到更新-->下载差异文件-->提取旧apk,合成-->校验文件MD5-->安装。
这里校验是有必要的,防止生成的apk无法安装,给出一个app上获取文件MD5的方法:
/**
* 获取单个文件的MD5值
*
* @param file 文件
* @return 文件MD5值
*/
public static String getFileMD5(File file) {
if (!file.isFile()) {
return null;
}
MessageDigest digest;
FileInputStream in;
byte buffer[] = new byte[1024];
int len;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer, 0, 1024)) != -1) {
digest.update(buffer, 0, len);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
return null;
}
BigInteger bigInt = new BigInteger(1, digest.digest());
return bigInt.toString(16);
}
声明:参考了Hongyang大神的博客:http://blog.csdn.net/lmj623565791/article/details/52761658,同时补充自己一些实际操作。