转载

增量更新

增量更新

现在大多数热门应用中都使用了增量更新来更新新的功能。比如解压微信或者抖音的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包进行完整性的校验。

原文  https://chsmy.github.io/2019/06/02/technology/增量更新/
正文到此结束
Loading...