Android中的增量更新指的是利用差分算法,计算两个App版本的差异,生成差分包,只需要下载差分包,不需要下载整个新版本的app,就可以完成App升级的方案,生成差分包不一定选用BSDiff(BSPatch,HPatch,XDelta等都可以),因为Android源码中使用的是BSDiff,所以本文简单分析BSDiff方式的增量更新。
流程图如下:
BSDiff简介,也可以直接百度"BSDiff":
这个库主要用于对比生成差分包,其中用到了一个叫bzip2
的库,用于文件压缩,其实Android源码这两个库都是存在的,虽然不能直接调用(因为开发者使用的是Android的SDK,不可能是完整的源码),Android源码中的路径版本\external\
目录下(这个目录下还有okhttp
,glide
.zxing
等等比较常见的库),版本过低可能会没有BSDiff库(Android6.0是有的)。
BSDiff的核心
尽可能多的利用old文件中已有的内容,尽可能少的加入新的内容来构建new文件(补丁),通常的做法是对old文件和new文件做子字符串匹配或使用hash技术,提取公共部分,将new文件中剩余的部分打包成patch包,在Patch阶段中,用copying和insertion两个基本操作即可 将old文件和patch包合成new文件(新Apk)
BSDiff基本步骤
- 对old文件中所有子字符串形成一个字典
- 对比old文件和new文件,产生diff string和extra string
- 将diffstring 和extrastring以及相应的控制字用zip压缩成一个patch包
服务端使用BSDiff
下载BSDiff,在Linux环境下使用,这里有个坑,BSDiff解压后的Makefile
文件可能存在格式问题,如果make
命令不能执行,修改后如下:
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
然后执行make
,生成bsdiff
和bspatch
文件,bsdiff
就是用来做差分的,实际运用中应该是服务端做差分,bspatch
是用来做合成的,就是Android端拿到差分包之后,和旧的apk文件合并生成新的apk。
可以通过bsdiff oldfile newfile patchfile
命令生成差分包文件。如:
#patch为生成的差分包文件
./bsdiff old.apk new.apk patch
Android合成新的APK文件
①新建cpp项目,将bspatch.c
文件和bzip2
相关的文件放在cpp
目录下:
bzip2
库中的文件比较多,demo中用到部分,可以直接查看bzip2
下的Makefile文件,确定需要使用的文件(这里没有头文件)② 在CMakeLists文件中引用,代码如下:
cmake_minimum_required(VERSION 3.4.1)
#导入bzip
file(GLOB bzib_source ${CMAKE_SOURCE_DIR}/bzip/*.c)
add_library(
native-lib
SHARED
native-lib.cpp
bspatch.c
#引用bzip
${bzib_source})
find_library(
log-lib
log)
target_link_libraries(
native-lib
${log-lib})
注意:
导入引用同步后,bspatch.c
文件中的#include <bzlib.h>
导入头文件可能有问题(不同版本),如果有问题将该行注释掉,下面爆红的地方快捷键导下包即可(就是正确引入头文件 如: #include "bzip/bzlib.h"
)
③Java层的逻辑,比较简单,这里没有请求服务端,本地取old.apk
和patch
文件,交给JNI
合成新的apk文件bspatck.apk
,然后执行安装操作,这里注意下8.0以上手机需要开启未知来源安装权限
,跳转安装的逻辑在InstallUtils
中,代码不贴了,需要完整代码的同学可以看文末链接,贴下MainActivity
的代码
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tVersion = findViewById(R.id.tv_version);
tVersion.setText(BuildConfig.VERSION_NAME);
final Button btUpdate = findViewById(R.id.bt_update);
btUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
requestPerms();
}
});
}
private void requestPerms() {
//权限,简单处理下
if (Build.VERSION.SDK_INT>Build.VERSION_CODES.N) {
String[] perms= {Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (checkSelfPermission(perms[0]) == PackageManager.PERMISSION_DENIED) {
requestPermissions(perms,200);
}else {
update();
}
}
}
//从本地内存取patch差分文件和旧的apk文件合并
private void update() {
new AsyncTask<Void, Void, File>() {
@Override
protected File doInBackground(Void... voids) {
//私有目录下的apk文件
String oldApk = getApplicationInfo().sourceDir;
//获取差分包的绝对路径
String patch= new File(Environment.getExternalStorageDirectory(),"patch").getAbsolutePath();
//合成的新apk的存放路径
String output=createNewApk().getAbsolutePath();
bsPatch(oldApk,patch,output);
return new File(output);
}
@Override
protected void onPostExecute(File file) {
//合成后安装
InstallUtils.installApk(MainActivity.this,file);
}
}.execute();
}
private File createNewApk() {
File newApk = new File(Environment.getExternalStorageDirectory(),"bspatch.apk");
if (!newApk.exists()) {
try {
//占位
newApk.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return newApk;
}
public native void bsPatch(String oldApk,String patch,String output);
④JNI
合成新的安装包,代码比较简单,直接贴一下吧,就是使用bspatch
直接合成
注意:
代码中调用的bspatch_main
方法就是bspatch
的main
方法,重命名了一下,建议改下名,养成好习惯,因为实际项目中可能用到多个JNI
库,都有main
方法
#include <jni.h>
#include <string>
//导入bspatch.c中要使用的方法
extern "C"{
int bspatch_main(int argc, char **argv);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jni_bsdiffdemo_MainActivity_bsPatch(JNIEnv *env, jobject instance, jstring oldApk_,
jstring patch_, jstring output_) {
//相当于类型转换,转换成C/C++识别字符串
const char *oldApk = env->GetStringUTFChars(oldApk_, 0);
const char *patch = env->GetStringUTFChars(patch_, 0);
const char *output = env->GetStringUTFChars(output_, 0);
const char *arg[]={"bisdff",oldApk,output,patch};
//第一个参数传4(否则直接抛错),第二个参数是数组如上
bspatch_main(4, const_cast<char **>(arg));
//释放指针
env->ReleaseStringUTFChars(oldApk_, oldApk);
env->ReleaseStringUTFChars(patch_, patch);
env->ReleaseStringUTFChars(output_, output);
}
最终实现效果如图,为了展示效果,new.apk
相对于old.apk
改了下versionName
:
完整代码请移步:Github
注:所用到的bzip2库
和bsdiff库
文件放在了bsdiff
目录下~