原理
获取文件差异:遍历两个字节文件old和new,比对每个字节的差异,并记录差异字节的文件索引,然后把差异的字节内容和索引记录到一个差异文件patch里。
合并差异:解析上面生成的patch文件,根据记录差异的索引和内容,修改或添加到old文件,修改后的old文件的内容就跟new文件的一样了。
重新安装apk:把合并后的apk文件重新安装后,这样就实现了增量更新的功能。
这样做的好处是不管你修改的是java代码,还是资源文件,还是代码混淆等都能正确的合成(前提是生成差异文件和合并差异文件时没出错)需要的新版apk文件,因为它是直接比对字节。
工具
要实现差异比对和差异合并,这里用到的工具是:bsdiff,官网地址:http://www.daemonology.net/bsdiff/。bsdiff是用c语言写的,它生成的差异文件会使用bzlib进行压缩,所以差异文件的体积也会相应比较小。
从官网下载下载的bsdiff是个压缩文件,解压后可以看到里面是源码文件,需要在编译后才能使用,Mac系统下的编译步骤:
1、打开终端,进入到bsdiff解压后的目录
2、本文下载的是bsdiff-4.3版本,它的Makefile文件有语法错误,需要修改后才能进行编译。
在终端输入:vim 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
3、在终端输入:make
【注意】bsdiff依赖于bzlib,所以如果编译提示找不到bzlib.h头文件,那么需要下载bzlib,下面提供三种操作系统的下载方式:
Ubuntu: apt install libbz2-dev
CentOS: yum -y install bzip2-devel.x86_64
Mac: brew install bzip2
4、成功编译后,会在当前目录生成两个文件:bsdiff和bspatch。bsdiff是用来生成差异文件的,bspatch是用来合并差异文件的。
使用bsdiff生成差异文件(一般在服务器上):
./bsdiff old.apk new.apk patch
参数1:旧的文件
参数2:新的文件
参数3:旧文件和新文件的差异部分的记录文件
使用bspatch合并差异文件(一般在客户端上):
./bspatch old.apk new.apk patch
参数1:旧的文件
参数2:新的文件
参数3:旧文件和新文件的差异部分的记录文件
【提示】可以对合并后的文件进行MD5校验,看是否跟new.apk一致。
集成到Android项目
打开Android Studio创建一个C/C++的工程,下面是工程结构:
其中:
/main/cpp/bsdiff4目录下放的是bsdiff的两个源码文件
/main/cpp/bzip2-1.0.5目录是bsdiff需要依赖的bzlip精简后的源文件
androidLog.h:在natvie层打印Log用
native-lib.cpp:实现生成和合并差异包的源文件
CMakeList.txt:构建so库的配置
BsDiffPatch:java层调用生成和合并差异包
下面是具体核心代码:
native-lib.cpp文件:
#include <jni.h>
#include <string>
extern "C" {
// 声明bsdiff的两个生成差异包和合并差异包的函数
extern int patch_main(int argc, char * argv[]);
extern int diff_main(int argc,char *argv[]);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_log_bsdiff_BsDiffPatch_diff(JNIEnv *env, jobject thiz, jstring old_apk, jstring new_apk, jstring patch) {
const char *_old_apk = env->GetStringUTFChars(old_apk, 0);
const char *_patch = env->GetStringUTFChars(patch, 0);
const char *_new_apk = env->GetStringUTFChars(new_apk, 0);
// 调用bsdiff合并差异包函数
const char *argv[] = {"", _old_apk, _new_apk, _patch};
diff_main(4, const_cast<char **>(argv));
env->ReleaseStringUTFChars(old_apk, _old_apk);
env->ReleaseStringUTFChars(patch, _patch);
env->ReleaseStringUTFChars(new_apk, _new_apk);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_log_bsdiff_BsDiffPatch_mergeApk(JNIEnv *env, jobject thiz, jstring old_apk, jstring new_apk, jstring patch) {
const char *_old_apk = env->GetStringUTFChars(old_apk, 0);
const char *_patch = env->GetStringUTFChars(patch, 0);
const char *_new_apk = env->GetStringUTFChars(new_apk, 0);
// 调用bsdiff合并差异包函数
const char *argv[] = {"", _old_apk, _new_apk, _patch};
patch_main(4, const_cast<char **>(argv));
env->ReleaseStringUTFChars(old_apk, _old_apk);
env->ReleaseStringUTFChars(patch, _patch);
env->ReleaseStringUTFChars(new_apk, _new_apk);
}
CMakeList.txt文件:
cmake_minimum_required(VERSION 3.10.2)
file(GLOB bzip bzip2-1.0.5/*.c)
file(GLOB bsdiff4 bsdiff4/*.c)
add_library( # Sets the name of the library.
native-lib
SHARED
native-lib.cpp
${bzip}
${bsdiff4})
include_directories(bzip2-1.0.5)
find_library( # Sets the name of the path variable.
log-lib
log)
target_link_libraries( # Specifies the target library.
native-lib
${log-lib})
java层代码:
package com.log.bsdiff;
public class BsDiffPatch {
static {
System.loadLibrary("native-lib");
}
/**
* 根据旧的apk文件和新的apk文件进行对比,生成差异包
* @param oldApk 旧版本apk包路径
* @param newApk 合成后新的apk包路径
* @param patch 差异包路径
*/
public native void diff(String oldApk, String newApk, String patch);
/**
* 将旧的apk包和指定的差异包合并成新版本的apk包
* @param oldApk 旧版本apk包路径
* @param newApk 合成后新的apk包路径
* @param patch 差异包路径
*/
public native void mergeApk(String oldApk, String newApk, String patch);
}
package com.log.bsdiff;
import android.content.Context;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class Util {
/**
* 模拟从服务器下载资源apk包
*
* @param context
* @param is
*/
public static String download(Context context, InputStream is, String fileName) {
File file = context.getDir("resources", Context.MODE_PRIVATE);
// File file = Environment.getExternalStorageDirectory();
String resourceFilePath = file.getAbsolutePath() + "/" + fileName;
FileOutputStream fos = null;
try {
File resourceFile = new File(resourceFilePath);
if (resourceFile.exists()) {
resourceFile.delete();
}
fos = new FileOutputStream(resourceFile.getAbsolutePath());
byte[] buf = new byte[2048];
int len;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return resourceFilePath;
}
}
package com.log.bsdiff;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private BsDiffPatch diffPatch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
diffPatch = new BsDiffPatch();
}
public void onUpdate(View view) {
mergerApk();
}
public void mergerApk() {
String oldApk = "/data/user/0/com.log.bsdiff/app_resources/old.apk";
String newApk = "/data/user/0/com.log.bsdiff/app_resources/new.apk";
String patch = "/data/user/0/com.log.bsdiff/app_resources/patch";
diffPatch.mergeApk(oldApk, patch, newApk);
}
public void diff(View view) {
String patch = "/data/user/0/com.log.bsdiff/app_resources/patch";
// 模拟下载文件
try {
String oldApk = Util.download(MainActivity.this,
getAssets().open("old.apk"), "old.apk");
String newApk = Util.download(MainActivity.this,
getAssets().open("new.apk"), "new.apk");
diffPatch.diff(oldApk, newApk, patch);
} catch (IOException e) {
e.printStackTrace();
}
}
public void onInstall(View view) {
installApk(new File("/data/user/0/com.log.bsdiff/app_resources/new.apk"));
}
private void installApk(File file) {
if (file == null && !file.exists()) {
Toast.makeText(MainActivity.this, "文件为空或不存在", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri fileUri = Uri.fromFile(file);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
MainActivity.this.startActivity(intent);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="版本2" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="生成差异包"
android:onClick="diff"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="更新"
android:onClick="onUpdate"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="安装新版apk"
android:onClick="onInstall"/>
</LinearLayout>