增量更新
现在大多数热门应用中都使用了增量更新来更新新的功能。比如解压微信或者抖音的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
修改完后继续编译还是报错,找不到<bzlib.h>,因为bsdiff依赖了bzip库
fatal error: bzlib.h: No such file or directory #include <bzlib.h>
下安装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 <jni.h> #include <string> #include<android/log.h> // 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包进行完整性的校验。