35

增量更新

 4 years ago
source link: https://www.tuicool.com/articles/iuq2Q3Z
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

增量更新

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK