一、概述

bsdiff 是一个差量更新算法,算法原理是尽可能多的利用 old 文件中已有的内容,尽可能少的加入新的内容来构建 new 文件。通常的做法是对 old 文件和 new 文件做子字符串匹配或使用 hash 技术,提取公共部分,将 new 文件中剩余的部分打包成 patch 包。在 Patch 阶段,用 copying 和 insertion 两个基本操作即可将 old 文件和 patch 包合成 new 文件(需要记录增加的内容及其在文件的偏移地址两个内容)。对于发生更改的部分,直接在该位置生成新的内容,而未更改的部分写 0 表示 new 文件没有变更这里。

bsdiff 为我们提供了两个工具 bsdiff 和 bspatch,前者用来生成差分包,后者可以将 old 文件和 patch 包合并成新文件。基于此,我们用 bsdiff 实现 Android 的增量更新的步骤为:

  1. 服务器用 bsdiff 生成 oldApk 与 newApk 的差分包 patch
  2. 终端下载 patch,通过 bspatch 将其与 oldApk 合并成为 newApk
  3. 安装 newApk 完成增量更新

Demo 不会涉及服务器和网络下载的内容,仅是手动生成 patch 并直接放到手机存储中来模拟这个过程。

二、编译 bsdiff 工具

bsdiff 提供的两个工具 bsdiff 和 bspatch 需要编译 bsdiff 源码才能生成,源码的下载地址:http://www.daemonology.net/bsdiff/,解压后得到 5 个文件:

增量更新 postgresql 增量更新算法_#include


接下来就是在 Linux 中编译这些源文件了,过程中会出现几次错误,我们一并说下。

运行 make 命令编译这些源码,会直接报错:

[root@iZwz9e9jwy11blescn79jyZ bsdiff-4.3]# make
Makefile:13: *** missing separator.  Stop.

打开 Makefile 去看 13 行,发现 install 指令后的 13、15 两行代码处于行首:

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

Makefile 的语法规则是指令后的每一行代码都要跟在 TAB 之后,所以这里在 13、15 行代码前加个 TAB 即可解决。

再次 make 又报错:

cc -O3 -lbz2    bsdiff.c   -o bsdiff
bsdiff.c:33:10: fatal error: bzlib.h: No such file or directory
 #include <bzlib.h>
          ^~~~~~~~~
compilation terminated.
make: *** [<builtin>: bsdiff] Error 1

缺少头文件<bzlib.h>,需要安装。不同系统的安装方法:

  • Ubuntu: apt install libbz2-dev
  • Centos: yum -y install bzip2-devel.x86_64
  • Mac: brew install bzip2

如果使用的是 Ubuntu 系统,用以上命令安装时可能会提示无法连接到 Ubuntu 服务器,需要添加 DNS 服务器地址,用 sudo vim /etc/resolv.conf 命令添加代码 nameserver 8.8.8.8 即可。

Centos 到这里应该就可以编译成功了,但是 Ubuntu 可能还会报一个编译错误:

cc -O3 -lbz2    bsdiff.c   -o bsdiff
/usr/bin/ld: /tmp/ccBe1tk5.o: in function `main':
bsdiff.c:(.text.startup+0x2aa): undefined reference to `BZ2_bzWriteOpen'
/usr/bin/ld: bsdiff.c:(.text.startup+0x888): undefined reference to `BZ2_bzWrite'
/usr/bin/ld: bsdiff.c:(.text.startup+0x8be): undefined reference to `BZ2_bzWrite'
/usr/bin/ld: bsdiff.c:(.text.startup+0x90a): undefined reference to `BZ2_bzWrite'
/usr/bin/ld: bsdiff.c:(.text.startup+0xb5c): undefined reference to `BZ2_bzWriteClose'
/usr/bin/ld: bsdiff.c:(.text.startup+0xbb6): undefined reference to `BZ2_bzWriteOpen'
/usr/bin/ld: bsdiff.c:(.text.startup+0xbde): undefined reference to `BZ2_bzWrite'
/usr/bin/ld: bsdiff.c:(.text.startup+0xc01): undefined reference to `BZ2_bzWriteClose'
/usr/bin/ld: bsdiff.c:(.text.startup+0xc58): undefined reference to `BZ2_bzWriteOpen'
/usr/bin/ld: bsdiff.c:(.text.startup+0xc80): undefined reference to `BZ2_bzWrite'
/usr/bin/ld: bsdiff.c:(.text.startup+0xca3): undefined reference to `BZ2_bzWriteClose'
collect2: error: ld returned 1 exit status
make: *** [<builtin>: bsdiff] Error 1

解决方法是在 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
        cc bsdiff.c ${CFLAGS} -o bsdiff #增加
bspatch:        bspatch.c
        cc bspatch.c ${CFLAGS} -o bspatch #增加

install:
        ${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin
        ifndef WITHOUT_MAN
        ${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1
        :endif

解决完以上错误,终于编译成功了,会生成两个可执行文件 bsdiff 和 bspatch:

增量更新 postgresql 增量更新算法_Ubuntu_02

三、生成 patch

执行如下命令可以生成补丁文件 patch:

./bsdiff old.apk new.apk patch

将这个 patch 存入手机中,以模拟从服务器下载 patch 的过程。

四、合成新 apk 并安装

Android 这边要将当前运行的 apk 与 patch 通过 bspatch 合并得到新 apk 并安装,Android 使用 bspatch 功能不能像 Linux 那样编译出一个可执行文件然后通过命令执行的方式合并,而是要通过调用源文件 bspatch.c 中的函数实现合并。

1.项目配置

导入文件

首先就是要将 bspatch.c 文件拷贝到 cpp 目录下,该文件依赖的头文件 bzlib.h 是 bzip2 的文件,所以还需要下载 bzip2 解压并导入相关文件。具体需要导入哪些文件,看 Makefile:

libbz2.a: $(OBJS)
	rm -f libbz2.a
	$(AR) cq libbz2.a $(OBJS)
	@if ( test -f $(RANLIB) -o -f /usr/bin/ranlib -o \
		-f /bin/ranlib -o -f /usr/ccs/bin/ranlib ) ; then \
		echo $(RANLIB) libbz2.a ; \
		$(RANLIB) libbz2.a ; \
	fi

最终编译出来的是 libbz2.a 文件,过程中需要 OBJS 这个变量:

OBJS= blocksort.o  \
      huffman.o    \
      crctable.o   \
      randtable.o  \
      compress.o   \
      decompress.o \
      bzlib.o

而 OBJS 表示的是上述多个 .o 文件,这些 .o 文件是编译的中间产物,是由同名的 .c 文件编译出来的。所以上述各个 .o 文件对应的 .c 文件都需要拷贝到项目中,另外它们中用到的头文件也需要一同拷贝。拷贝之后项目当前目录结构如下:

增量更新 postgresql 增量更新算法_增量更新 postgresql_03

改写 CMakeLists

cmake_minimum_required(VERSION 3.4.1)

# bzip变量表示后面路径下所有的.c文件
file(GLOB bzip bzip2/*.c)
add_library( native-lib
             SHARED
             native-lib.cpp
             # 编译 bspatch.c 和 bzip 变量指向的文件
             bspatch.c
             ${bzip})

find_library( log-lib
              log )

target_link_libraries( native-lib
                       ${log-lib} )

这样开始编译后会有一个编译错误:

app\src\main\cpp\bspatch.c:31:10: fatal error: 'bzlib.h' file not found

去看 bspatch.c 文件找第 31 行发现 bzlib.h 是用尖括号的形式引入的:

#include <bzlib.h>

c 语言中用尖括号引入表示该头文件是系统文件,但是很显然 bzlib.h 是我们手动拷贝到项目中的,不是系统文件。这里有两种解决方法:

  1. 修改 bspatch.c 文件,将 #include <bzlib.h> 修改为 #include “bzlib.h”
  2. 如不想修改 bspatch.c ,则可以在 CMakeLists 文件中加一句话:include_directories(bzip2),这样就相当于将 bzip2 目录下的文件添加到 <> 的寻找范围之内了。

2.合成新 apk

调用 bspatch.c 的 main() 函数,传入 old.apk、new.apk 和 patch 的路径就可以将 old.apk 和 patch 合并成 new.apk。Java 代码这边用 BsPatcher 完成对 c 的调用:

public class BsPatcher {

    static {
        System.loadLibrary("native-lib");
    }

    public static native boolean bsPatch(String oldApk, String patch, String newApk);
}

native-lib.cpp 主要工作就是调用 bspatch.c 的 main():

#include <jni.h>
#include <string>
#include <android/log.h>

extern "C" {
// 兼容 C,拿到 bspatch.c 的 main()
extern int main(int argc, char *argv[]);
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_frank_deltalupdate_BsPatcher_bsPatch(JNIEnv *env, jclass clazz, jstring old_apk,
                                              jstring patch_, jstring new_apk) {
    char *oldApk = (char *) (env->GetStringUTFChars(old_apk, 0));
    char *patch = (char *) (env->GetStringUTFChars(patch_, 0));
    char *newApk = (char *) (env->GetStringUTFChars(new_apk, 0));

    // 打个 log 看看获取的路径是否正确
    __android_log_print(ANDROID_LOG_ERROR,"oldApk","%s",oldApk);
    __android_log_print(ANDROID_LOG_ERROR,"patch","%s",patch);
    __android_log_print(ANDROID_LOG_ERROR,"newApk","%s",newApk);
    
    char *argv[] = {"", oldApk, newApk, patch};
    int result = main(4, argv);

    // 释放
    env->ReleaseStringUTFChars(old_apk, oldApk);
    env->ReleaseStringUTFChars(patch_, patch);
    env->ReleaseStringUTFChars(new_apk, newApk);

    // 合并成功返回 true
    return result == 0;
}

UI 这边,因为合并文件是耗时操作,所以调用 BsPatcher.bsPatch() 的工作要放在子线程中:

public class MainActivity extends AppCompatActivity {

	...

	// 点击更新按钮触发
    public void update(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // patch 和 new.apk 都会放在下面这个路径中:
                // /sdcard/Android/data/包名/files/apk
                File newApk = createNewApk();
                File patch = new File(getExternalFilesDir("apk"), "patch");
                if (BsPatcher.bsPatch(getApplicationInfo().sourceDir, patch.getAbsolutePath(),
                        newApk.getAbsolutePath())) {
                    // 如果合并成功就走安装流程
                    install(newApk);
                }
            }
        }).start();
    }

    private void install(File apk) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Uri apkUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", apk);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(apk), "application/vnd.android.package-archive");
        }
        startActivity(intent);
    }

    private File createNewApk() {
        File newApk = new File(getExternalFilesDir("apk"), "new.apk");
        if (!newApk.exists()) {
            try {
                newApk.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return newApk;
    }
}

效果图:

增量更新 postgresql 增量更新算法_Ubuntu_04


注意处理权限问题,不同的系统和环境如果用以上代码测试失败了,可能是没有申请 WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES 权限。