版本信息
Binary diff/patch utility: 4.3
编译bsdiff系统:centos
AndroidStudio:3.5
增量更新原理
新的apk包与老的apk包通过bsdiff工具生成增量更新包,下载增量更新包之后, 通过bspatch工具生成全量安装包。安装全量包即达到了增量更新的目的。
编译bsdiff
在linux中下载bsdiff,通过make命令编译。由于网站上下载的MakeFile编写问题,会报错。格式错误, shell语法的指令前面要有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
修改之后在执行make命令,如果报以下错误, 说明bzlib没有安装。根据自己的系统安装bzlib。
bsdiff.c:33:19: fatal error: bzlib.h: No such file or directory
#include <bzlib.h>
^
compilation terminated.
安装完成再执行make命令,生成以下文件则编译成功。
cc -O3 -lbz2 bsdiff.c -o bsdiff
cc -O3 -lbz2 bspatch.c -o bspatch
生成差分包
通过命令生成差分包patch,自此服务器端的工作结束。android端需要下载这个差分包,通过bspatch工具合成全量包安装。
./bsdiff old.apk new.apk patch
Android端
新建C++项目,bsdiff源码中的bspatch.c文件放到项目中。将因为bspatch同样依赖bzlib,所以需要将bzlib的源码也下载下来供使用。下载地址可用文字开始的链接。最后项目的目录结构会如下图所示(bzlib源码已剔除不需要的部分):
编辑CMakeLists.txt,引入bzlib库和bspatch,完整代码如下所示:
cmake_minimum_required(VERSION 3.4.1)
file(GLOB bzip bzlib/*.c)
add_library(
native-lib
SHARED
${bzip}
bspatch.c
native-lib.cpp)
include_directories(bzlib)
find_library(
log-lib
log)
target_link_libraries(
native-lib
${log-lib})
编写BsPatcher工具类,提供java调用的native方法,如下所示:
public class BsPatcher {
static {
System.loadLibrary("native-lib");
}
/**
* 生成增量更新包
* @param oldApkPath 老apk路径
* @param newApkPath 生成的全量包路径
* @param patchPath 从服务器上获取的差量包路径
*/
public static native void update(String oldApkPath, String newApkPath, String patchPath);
}
编写native-lib.cpp文件,编写native方法,调用bspatch的main方法实现用old.apk和差分包合成全量包。代码如下所示(xxx是包路径中的一部分,涉及敏感信息,用xxx代替):
#include <jni.h>
#include <string>
extern "C" {
extern int main(int argc, const char *argv[]);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_xxx_hotfixdemo_BsPatcher_update(JNIEnv *env, jclass clazz, jstring old_apk_path,
jstring new_apk_path, jstring patch_path) {
const char *oldPath = env->GetStringUTFChars(old_apk_path, 0);
const char *newPath = env->GetStringUTFChars(new_apk_path, 0);
const char *patch = env->GetStringUTFChars(patch_path, 0);
const char *argv[] = {"", oldPath, newPath, patch};
main(4, argv);
env->ReleaseStringUTFChars(old_apk_path, oldPath);
env->ReleaseStringUTFChars(new_apk_path, newPath);
env->ReleaseStringUTFChars(patch_path, patch);
}
编写MainActivity类,通过点击事件触发更新,为了简化下载差分包的过程,此demo将差分包已经放到SD卡的根目录。合成全量包是耗时操作,需要放到子线程工作,本demo使用AsynTask实现。代码如下所示:
@SuppressLint("StaticFieldLeak")
public void update(View view) {
new AsyncTask<Void, Void, File>() {
@Override
protected File doInBackground(Void... voids) {
String patch = new File(Environment.getExternalStorageDirectory(), "patch").getAbsolutePath();
String oldApk = getApplicationInfo().sourceDir;
String newApk = createNewApk().getAbsolutePath();
BsPatcher.update(oldApk, newApk, patch);
return new File(newApk);
}
@Override
protected void onPostExecute(File file) {
super.onPostExecute(file);
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
/* Android N 写法*/
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(MainActivity.this, BuildConfig.APPLICATION_ID + ".fileprovider", file);
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
/* Android N之前的老版本写法*/
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
}.execute();
}
private File createNewApk() {
File newApk = new File(Environment.getExternalStorageDirectory(), "bsdiff.apk");
if (!newApk.exists()) {
try {
newApk.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return newApk;
}
最终实现效果,点击升级,直接用patch和old.apk生成全量包,然后拉起安装程序进行安装。
注意:
需要读取权限,Android6.0及以上版本需要动态申请权限
Android7.0及以上安装需要FileProvider
Android8.0及以上安装需要在AndroidManifest.xml中声明权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />