在Android中使用Bsdiff实现增量更新

使用BsDiff实现增量更新

Android中,我们应用内更新软件通常是下载完整的安装包,然后进行安装。但是当安装包很大的时候,每次更新都会让用户不爽,因为不仅会消耗很多流量,而且当用户网络不是很好的时候,更新就会很慢,而且会影响到用户体验,比如下载期间占用带宽导致加载图片缓慢等。因此,用户很可能会拒绝更新。

bsdiff就是一种差量算法,可以根据两个文件间的区别生成一份差量文件,然后根据旧文件和差量文件重新生成新文件。应用在Android中是这样的:用户安装的是v1.0版本,然后当更新v2.0版本时,服务端根据v1.0v2.0生成一个差量包patch,然后用户提示更新的时候去下载patch,再在本地根据已安装的版本v1.0patch合成v2.0版本然后进行安装更新。

编译服务端使用的bsdiff

在服务端,是可以直接安装bsdiff的,但是为了保持bsdiff版本与应用中的版本的一致,因此采用自己编译的方式。

下载源码

首先下载bsdiff的源码:官网地址 ,但是官网下载的时候居然提示403。因此我上传了一份到github上,可以从github下载或者从这里下载

然后下载bzip2的源码:从SourceForge下载,因为bsdiff需要使用到bzip2。

开始编译

Windows编译是很麻烦的,缺少相应的环境和工具,并且bsdiff中还引用了一些Linux中的头文件。所以这里选择在Linux中编译。

首先解压bsdiffbzip2,并将二者置于同一个目录中。

.
├── bsdiff-4.3
│   ├── bsdiff.1
│   ├── bsdiff.c
│   ├── bspatch.1
│   ├── bspatch.c
│   └── Makefile
├── bzip2-1.0.6
...
复制代码

然后修改bsdiff中的Makefile,因为bsdiff引用了bzip2的头文件和库文件,所以需要将搜索路径指向我们解压后的bzip2-1.0.6。同时,Makefile中还有一些格式问题,同样需要修改。修改后的Makefile如下:

BZIP2PATH=../bzip2-1.0.6
CC=gcc

CFLAGS          +=      -O3 -lbz2 -L${BZIP2PATH} -I ${BZIP2PATH}
  
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
复制代码

改动不是很多,首先加了一个BZIP2PATH参数并指向bzip2的路径,然后在CFLAGS中指定库文件搜索目录-L${BZIP2PATH}和头文件搜索路径-I ${BZIP2PATH}bzip2路径。其次是指定了编译器为gcc,并且给bsdiffbspatch添加了明确的生成的命令。最后是在install命令中的.ifndef.endif前加了个tab缩进。

CFLAGS中,使用-lbz2链接了bz2库,所以需要先生成libbz2.a。切到bzip2-1.0.6目录中,然后执行命令:

# 因为只需要libbz2.a,所以其他的不需要编译
make libbz2.a
复制代码

此时在bzip2-1.0.6中可以看到生成了libbz2.a文件,然后切回bsdiff-4.3目录中执行命令:

make
复制代码

这时候,在bsdiff-4.3目录中就会生成bsdiffbspatch两个可执行文件了。实际上我们是不需要bspatch这个可执行文件的,因为合成步骤是在手机上完成的,服务端只需要使用bsdiff去生成patch差分文件即可。

所以可以使用命令:make bsdiff仅生成bsdiff可执行文件。

生成差分文件

使用刚才编译出的bsdiff去生成差分文件,后接三个参数,第一个是旧版本的文件,第二个是新版本的文件,第三个是生成的差分文件:

./bsdiff app-v1.apk app-v2.apk patch
复制代码

执行上述命令后就会生成patch文件,这个patch文件应该是小于app-v2.apk的。当更新时,用户只需要下载patch文件即可。以上就是整个服务端需要做的事了,就是编译bsdiff,然后生成差分文件。

在Android中使用bspatch合成安装包

bspatch是用于合成安装包的可执行文件。前面使用bsdiff将旧版本和新版本比较产生patch文件,这里的bspatch就是将旧版本和patch合并成新版本文件,与bsdiff是一个对应的过程,也是Android上主要使用的方法。

# 参数顺序和bsdiff是一样的
./bspatch apk-v1.apk apk-v2.apk patch
复制代码

引入源文件

Android中使用也是比较简单的,首先新建一个native项目或者nativelib。然后在src/main/cpp目录下,创建一个目录bzip2-1.0.6。将对应的bzip2源文件放在这里。

注意,并不需要放入bzip2解压后的所有文件,而是生成libbz2.a相关的源文件即可。可以在bzip2-1.0.6解压后的目录中查看Makefile文件:

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

libbz2.a: $(OBJS)
        rm -f libbz2.a
        $(AR) cq libbz2.a $(OBJS)
        
blocksort.o: blocksort.c
        @cat words0
        $(CC) $(CFLAGS) -c blocksort.c
huffman.o: huffman.c
        $(CC) $(CFLAGS) -c huffman.c
crctable.o: crctable.c
        $(CC) $(CFLAGS) -c crctable.c
randtable.o: randtable.c
        $(CC) $(CFLAGS) -c randtable.c
compress.o: compress.c
        $(CC) $(CFLAGS) -c compress.c
decompress.o: decompress.c
        $(CC) $(CFLAGS) -c decompress.c
bzlib.o: bzlib.c
        $(CC) $(CFLAGS) -c bzlib.c
复制代码

上面是从Makefile中截取的一部分,从中可以看出我们需要blocksort.c、huffman.c、crctable.c、randtable.c、compress.c、decompress.c、bzlib.c七个文件,同时还需要两个头文件bzlib.hbzlib_private.h。也就是一共9个文件,放入上述新建的zip2-1.0.6目录中。然后将bsdiff解压后的bspatch.c放入src/main/cpp中。

现在的目录结构应该是这样的:

.
├── src
│   ├── main
│   	├── cpp
│   		├── bzip2-1.0.6
|			├── bspatch.c
│   		├── nativelib.cpp
│   		└── CMakeLists.txt
├
...
复制代码

其中nativelib.cpp是新建module的时候自动生成的,可以修改成其他文件名,比如这里我就修改成了bspatch_merge.cpp

编写CMakeLists.txt

然后编写CMakeLists.txt规则,将bzip2的源文件以及bspatch的源文件都添加进去:

cmake_minimum_required(VERSION 3.10.2)
project("bspatch")

file(GLOB bzip_sources ${CMAKE_SOURCE_DIR}/bzip2-1.0.6/*.c)

add_library(
    bspatch
    SHARED

    bspatch.c
    bspatch_merge.cpp
    ${bzip_sources}
)


find_library(
    log-lib
    log
)


target_link_libraries(
    bspatch
    ${log-lib}
)
复制代码

bspatch.c中,入口方法也就是main函数,因为在Linux下最终是将bspatch.c编译成可执行文件的。而在Android中,我们最终是将它编译成一个共享库so,因此最好将main函数重命名一下,避免以后添加其他库的时候又有main函数导致冲突。这里将其改为patch_main。 并且,还需要将bspatch.c中引用的头文件#include<bzlib.h>改为#include "bzip2-1.0.6/bzlib.h"

编写代码

然后将NativeLib类重命名,改为PatchUtils,并定义成一个单例类:

object PatchUtils {

	init {
        // 这里的名字必须与CMakeLists.txt中的add_library中定义的一致
        System.loadLibrary("bspatch")
    }

    /**
     * 注意,该方法是一个耗时操作,不要放到主线程中去。
     *
     * 根据旧文件和差分包文件合并成新的文件
     * [newFile] 合并后的文件,应该是一个具体的文件路径
     * [oldFile] 旧文件的路径,应该是一个具体的文件路径
     * [patch]   差分包文件,应该是一个具体的文件路径
     *
     * 合并成功则返回true,否则返回false
     */
    external fun bsPatch(newFile: String, oldFile: String, patch: String): Boolean
}
复制代码

此时bsPatch方法应该是报红色错误的,鼠标放在上面根据提示可以直接生成jni方法,选择生成文件位置的时候记得选择bspatch.c中。或者不让他生成,直接在bspatch.c中手写即可,这样的话需要注意方法中的包名和类名要保持一致。

#include <jni.h>
#include <string>

extern "C" {
extern int patch_main(int argc, char *argv[]);
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_study_bspatch_PatchUtils_bsPatch(JNIEnv *env, jobject thiz, jstring new_file,
                                          jstring old_file, jstring patch_file) {
    const char *newFile = env->GetStringUTFChars(new_file, nullptr);
    const char *oldFile = env->GetStringUTFChars(old_file, nullptr);
    const char *patchFile = env->GetStringUTFChars(patch_file, nullptr);

    char *argv[] = {"", const_cast<char *>(oldFile), const_cast<char *>(newFile),
                    const_cast<char *>(patchFile)};
    int res = patch_main(4, argv);

    env->ReleaseStringUTFChars(old_file, oldFile);
    env->ReleaseStringUTFChars(new_file, newFile);
    env->ReleaseStringUTFChars(patch_file, patchFile);

    return res == 0;
}
复制代码

首先通过extern关键字引入bspatch.c中的patch_main方法,然后调用。在可执行文件中,我们使用./bspatch old.apk new.apk patch命令去生成新文件,而对应的方法中,参数实际上是4个,因为第一个参数是函数本身,这里是需要注意的。

到这里就已经完成了Android中的引入了,使用的时候直接调用PatchUtils.bsPatch方法即可。当前安装的apk可以通过context.applicationInfo.sourceDir去获取。

详细代码上传至github仓库上了。

总结

使用bsdiff进行安装包的增量更新并不难,甚至可以说是非常简单,因为我们实际上在Android中仅仅是去调用bspatch中的main方法去合成而已。同样的,Linux编译bsdiff也很简单,只是稍微修改一下Makefile就行了。

使用bsdiff可以有效的降低更新时下载的安装包的体积,因为只需要下载对应的patch分包即可,而不需要下载完整的安装包文件,这也是我们最终的目的。

但是,实际使用中却很麻烦,因为每次更新后,都需要和之前的所有旧版本apk生成对应的patch分包,然后在获取更新信息的时候,根据传递的版本参数返回对应的patch下载地址。

这只是一个渠道包的情况,实际上我们线上每个应用商店上传的包都是不同的渠道包,而各个应用商店大概有十来个。也就是说,每次升级,至少要产生十几个patch分包,并且这还只是和一个旧版本apk产生的,而实际中,我们又非常多的旧版本,这也就意味着,patch分包的文件数量将会非常多...

当然,可以编写脚本文件来管理....

分类:
Android
标签: