现在大多数热门应用中都使用了增量更新来更新新的功能。比如解压微信或者抖音的apk,在其lib文件夹下都能找到类似 libbspatch.so的动态库,这个就是用来增量更新的库。
Android NDK中为我们提供了一个工具可以查看动态库中的方法,工具在\sdk\ndk-bundle\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin
进入到此文件夹下面执行下面的方法就能看到so中的方法了。
arm-linux-androideabi-nm.exe -D so的路径
增量更新使用到一个开源库bsdiff,bsdiff是一个查分算法,原理是旧文件跟新文件对比,尽可能多的利用old文件中已经有的内容,尽可能少的加入新的内容来构建new文件。
通常的做法是对旧文件和新文件做字符串匹配或者使用hash技术提取公共部分,然后把新文件的剩余部分打成patch包(差分包中记录着新内容相对旧内容的偏移地址),在Patch阶段中用copying和insertion两个操作把旧文件和patch文件合成新文件。
增量更新的流程:在服务器端,使用bsdiff工具把旧的apk和新的apk进行比对得到差分包patch包,通过网络下载到本地,通过bspatch工具把本地旧的apk和patch包合成新的apk包。最后安装新的apk
bsdiff 下载地址:http://www.daemonology.net/bsdiff/ 现在最新的是bsdiff-4.3
将下载的文件上传到服务器,解压进入bsdiff-4.3文件夹,执行make命令编译文件,发现会出错。
是因为Makefile文件中的格式不正确
CFLAGS += -O3 -lbz2
PREFIX ?= /usr/local
INSTALL_PROGRAM ?= ${INSTALL} -c -s -m 555
INSTALL_MAN ?= ${INSTALL} -c -m 444
all: bsdiff bspatch
bsdiff: bsdiff.c
bspatch: bspatch.c
install:
${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin
.ifndef WITHOUT_MAN
${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1
.endif
//上面是错误的 .ifndef和.endif前面需要TAB键缩进一下
CFLAGS += -O3 -lbz2
PREFIX ?= /usr/local
INSTALL_PROGRAM ?= ${INSTALL} -c -s -m 555
INSTALL_MAN ?= ${INSTALL} -c -m 444
all: bsdiff bspatch
bsdiff: bsdiff.c
bspatch: bspatch.c
install:
${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin
.ifndef WITHOUT_MAN
${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1
.endif
修改完后继续编译还是报错,找不到
fatal error: bzlib.h: No such file or directory
#include
下安装bzip2
//Linux
yum install bzip2-devel.x86_64
//Ubuntu
apt install libbz2-dev
//Mac
brew install bzip2
然后在执行make命令,成功,bsdiff-4.3文件夹下面生成了两个可执行文件bsdiff和bspatch
把old.apk和new.apk,上传到此文件夹,执行下面命令就可以生成差分包
bsdiff old.apk new.apk patch
将patch差分包下载到手机中跟旧的apk合并成新的安装包。
手机方面需要把bspatch继承到我们的项目中才能合并
AndroidStudio中新建一个C++文件,前面解压缩的bsdiff-4.3中有bspatch.c文件,将他拷贝到cpp文件夹下面。
编译之后会报错,因为前面我们知道bsdiff依赖了bzip库,linux系统中我们可以直接安装,AndroidStudio中,我们需要自己下载编译,可以在Linux中变异成静态文件导入,不过由于它的文件比较少,我们可以直接导入源码。
bzip2的地址:https://sourceforge.net/projects/bzip2/
http://www.bzip.org/downloads.html
下载之后解压,我们看到里面的文件也是挺多的,我们并不需要全部的文件,那需要哪些呢。我们可以看到它有一个Makefile文件,打开它,从代码中可以看到
libbz2.a: $(OBJS)
OBJS= blocksort.o \
huffman.o \
crctable.o \
randtable.o \
compress.o \
decompress.o \
bzlib.o
libbz2.a 这个静态文件可以通过OBJS中的这些文件编译成,所以我们只需要这几个c文件就好了。cpp下新建一个bzip文件夹。把他们也复制到该文件夹加下
下一步配置CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
file(GLOB bzip_source ${CMAKE_SOURCE_DIR}/bzip/*.c)
add_library(
bspatcher
SHARED
${CMAKE_SOURCE_DIR}/bspatcher.cpp
${CMAKE_SOURCE_DIR}/bspatch.c
${bzip_source})
find_library(
log-lib
log)
target_link_libraries(
bspatcher
${log-lib})
bspatcher是我们自己的cpp文件
下面开始编写自己的java文件和bspatcher这个cpp文件
public class BsPatcher {
static {
System.loadLibrary("bspatcher");
}
/**
* 合成安装包
*
* @param oldApk 旧版本安装包,如1.1.1版本
* @param patch 差分包,Patch文件
* @param output 合成后新版本apk的输出路径
*/
public static native void bsPatch(String oldApk, String patch, String output);
}
bspatcher文件
#include
#include
#include
// extern 声明在 bspatch.c
extern "C" {
extern int p_main(int argc, const char *argv[]);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_chs_bsdiff_BsPatcher_bsPatch(JNIEnv *env, jclass type,
jstring oldApk_, jstring patch_,
jstring output_) {
// 将Java字符串转为C/C++的字符串,转换为UTF-8格式的char指针
const char *oldApk = env->GetStringUTFChars(oldApk_, 0);
const char *patch = env->GetStringUTFChars(patch_, 0);
const char *output = env->GetStringUTFChars(output_, 0);
__android_log_print(ANDROID_LOG_ERROR,"BSPATCH",oldApk,patch,output);
// bspatch, oldfile, newfile, patchfile
const char *argv[] = {"", oldApk, output, patch};
p_main(4, argv);
// 释放指向Unicode格式的char指针
env->ReleaseStringUTFChars(oldApk_, oldApk);
env->ReleaseStringUTFChars(patch_, patch);
env->ReleaseStringUTFChars(output_, output);
}
非常简单,从java层把old.apk的路径,patch包的路径,new.apk的说出路径传进来然后传入bspatch.c的main方法中即可完成合并。我们把p_main中的main方法改个名字改成p_main,方便和main方法区分。
最后在Activity中开启线程下载patch包到本地,合成新包,并安装新包,比如使用AsyncTask下载
new AsyncTask<Void, Void, File>() {
@Override
protected File doInBackground(Void... voids) {
String patch = new File(Environment.getExternalStorageDirectory(), "patch").getAbsolutePath();
// 获取旧版本路径(正在运行的apk路径)
String oldApk = getApplicationInfo().sourceDir;
String output = createNewApk().getAbsolutePath();
if (!new File(patch).exists()) {
return null;
}
BsPatcher.bsPatch(oldApk, patch, output);
return new File(output);
}
@Override
protected void onPostExecute(File file) {
super.onPostExecute(file);
Log.e("output---->>", "onPostExecute");
// 已经合成了,调用该方法,安装新版本apk
if (file != null) {
if (!file.exists()) return;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Uri fileUri = FileProvider.getUriForFile(MainActivity.this, MainActivity.this.getApplicationInfo().packageName + ".fileprovider", file);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
MainActivity.this.startActivity(intent);
} else {
Toast.makeText(MainActivity.this, "差分包不存在!", Toast.LENGTH_LONG).show();
}
}
}.execute();
private File createNewApk() {
File newApk = new File(Environment.getExternalStorageDirectory(), "new.apk");
if (!newApk.exists()) {
try {
newApk.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return newApk;
}
到这里就更新完成了。
缺点:
我们不能保证所有用户都能升级完成,比如我们最新的patch包是2.0版本和3.0版本差分出来的,如果用户此时用的1.0版本,那就无法升级成功,所以还要做一个1.0和3.0之间的差分包。随着版本的越来越多,需要做的差分包也越来越多。可以在Linux中写一个自动的脚本来完成。
如果差分包在下载的过程中被篡改也无法合成成功,可以下载完后通过md5 或者其他方式对patch包进行完整性的校验。