NDK开发—增量更新

这就是为什么有些很大的软件更新的时候更新包只有几M的原因。

Posted by 陈再峰 on January 23, 2018

1、普通更新和增量更新

首先了解一下应用普通更新的逻辑(这里指不通过应用市场更新): 新版本发布后将APK文件上传到服务器。然后由客户端下载新的APK文件并安装。

但是如果APK过大,普通更新的弊端就出现了。
比如:一个游戏的APK,老版本有480M。新版本添加了一个模块APK增加到500M。按照普通更新的逻辑,用户需要下载500M的APK,很显然比较费流量!这个并不是只针对用户,对服务器也是如此,服务器也需要节省带宽。

假如有一种机制:只需要下载新版APK与老版APK多出的不同的部分,然后再由客户端将老版的APK和下载的部分合并成新版APK,再进行安装更新。

例如这个例子,用户只需要下载20M左右的更新包就能进行更新操作,就能非常愉快的解决流量的这个问题了。

以上的这个例子就是增量更新的基本逻辑,用图形来表示:

增量更新

根据增量更新的逻辑,可以知道增量更新重要的几个知识点。

  • 服务器根据新旧APK生成的增量包。(本文重点)
  • 客户端下载差分包(本文不做介绍)。
  • 客户端根据旧APK和下载的差分包生成新的APK。(本文重点,另外需要一点点NDK的基础)
  • 客户端安装新的APK。

2、增量更新的依赖

增量更新需要差分APK和合并APK,这里并不需要应用的开发者去写算法,直接可用三方提供的工具。

官方地址: http://www.daemonology.net/bsdiff/

不过windows无法下载,可能被墙的缘故。我从第三方弄到的工具:

链接:拆分合并工具百度云地址
资源 这里集合了windows平台和linux平台使用的差分工具,以及客户端需要用到的合并工具。

3、差分APK

差分APK的操作有多种,我这里介绍windows平台和Linux平台两种。

3.1、Windows下的差分

将下载的工具中windows平台下的文件bsdiff4.3-win32-src.zip解压。 bsdiff for windows 然后再release文件夹下可以看到两个.exe文件。需要使用windows差分文件需要用到。bsdiff.exe. bsdiff 接下来将旧版的APK和新版的APK复制到这个文件夹下,执行以下命令:

bsdiff.exe  旧APK地址  新APK地址 增量包地址

执行拆分命令 然后增量包就生成了: 生成增量包 以上方法应该是非常简单了。

当然解压bsdiff4.3-win32-src.zip之后会发现这是源码: windows平台的源码

差分的效果 通过这个图可以看到,增量包要比新版本的安装包要小很多的。如果新版apk更大,效果更明显。

3.2、Linux下的差分

如果没有windows系统,linux系统同样是提供支持的。我这边是装了一个Ubuntu的虚拟机。由于linux不太熟练,我这里只展示linux差分的核心部分。

第一步将bsdiff-4.3.tar.gz和bzip2-1.0.6.tar.gz解压。将bsdiff-4.3.tar.gz中的bsdiff.c和bzip2-1.0.6.tar.gz中所有的.c和.h文件全部复制到一个目录中。(我这里是用windows解压的,所以放的是windows的截图) bsdiff.c bzip2的所有.h和.c 在Linux中新建一个文件夹存放这些文件: 在Linux的文件

然后需要用linux的编译命令gcc将这些文件编译成Linux中可执行的文件。(gcc是什么命令我就不介绍了)。接下来会踩几个坑。 接下来执行:

 gcc -g -o axe_bsdiff  blocksort.c decompress.c bsdiff.c  randtable.c  bzip2.c huffman.c compress.c bzlib.c crctable.c 

里面的axe_bsdiff是linux端可运行文件的文件名,可修改。执行这段命令就会遇到以下问题: 第一个坑 直接用vim进入bsdiff.c,找include bzlib.h的地方: vim bsdiff.c 问题一目了然了,只有引用系统提供的头文件才能用”< >”,那就把这里改成 include "bzlib.h" 然后保存!
这样也解决了上面为什么需要这么多文件的疑问了:bsdiff.c调用了bzlib下的c文件。 解决这个问题那就继续执行gcc命令。 第二个坑 以上就是第二个坑了,main方法被多次定义。这个问题也很清楚,每个程序只有一个main方法。那就把其他地方定义的main去掉就行了。这里需要保留bsdiff.c 中的main方法。

将不需要的main方法修改成其他方法

我这里用windows端的notepad++修改的。名字是随便改的,,只要符合规范就行了。 客户端合并也需要这些修改后的源文件,已上传git。最后有链接

修改完成之后,继续执行gcc编译命令。
然后就成功了,生成了axe_bsdiff的可执行文件! gcc成功

接下来的步骤就和windows端差分增量包操作差不多了。执行一下命令:

./axe_bsdiff 旧APK路径 新APK路径 增量包路径

我这里将新旧APK都赋值到了这个文件夹,然后执行命令: 执行差分命令 成功生成增量包: 成功生成增量包

总体来说Linux差分增量包要比windows差分增量包麻烦些。还可能会遇到权限不够的问题,这个时候需要chmod命令等等。

4、客户端的合并操作

这里还是需要用到bsdiff-4.3.tar.gz和bzip2-1.0.6.tar.gz的解压文件。这里需要用到bsdiff-4.3中的bspatch.c还有bzip2-1.0.6所有的.c和.h。上面讲到的Linux中差分增量包的文件,这里的可以直接使用。

4.1、新建NDK项目 (这个就不讲解了)
4.2、将所需要的文件导入并且配置好CMakeLists.txt。

文件导入

CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)

# 加载指定文件夹的所有文件
file(GLOB bzip2 src/main/cpp/bzip2/*.c)

find_library( log-lib
              log )

# 配置AxeBsPatch动态库
add_library( AxeBsPatch
             SHARED
             src/main/cpp/bspatch.c
             ${bzip2}
             )
# 链接AxeBsPatch动态库
target_link_libraries( AxeBsPatch
                       ${log-lib} 
4.3、新建native方法。

这边生成的native方法需要传入三个参数,老的APK地址、新的APK地址、下载的增量包的地址。

public class BsPatch {
    public native static int patch(String oldFile,String newFile,String patchFile);
    static {
        System.loadLibrary("AxeBsPatch");
    }
}

然后用javah命令生成.h文件。

4.4、实现.h文件中的方法

在bspatch.c中include生成的头文件。并实现.h中的方法。

#include "com_mg_axechen_bspatch_BsPatch.h"
// ... ... 省略若干代码

/*
 * Class:     com_mg_axechen_bspatch_BsPatch
 * Method:    patch
 * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
 * params: old_path:The path for old APK.
 *         new_path:The path for new APK.
 *         patch_path:The path for patch package.
 */
JNIEXPORT jint JNICALL Java_com_mg_axechen_bspatch_BsPatch_patch
        (JNIEnv *env, jclass jclz, jstring old_path, jstring new_path, jstring patch_path) {

    // 先将jstring转化为char
    char *oldPath = (char *) (*env)->GetStringUTFChars(env, old_path, NULL);
    char *newPath = (char *) (*env)->GetStringUTFChars(env, new_path, NULL);
    char *patchPath = (char *) (*env)->GetStringUTFChars(env, patch_path, NULL);
    char *argv[4];

    int ret = -1;

    // 第一个参数随便填写
    argv[0] = "AXE_BSPATCH";
    argv[1] = oldPath;
    argv[2] = newPath;
    argv[3] = patchPath;

    // 第一个参数必须为4.
    ret = main(4, argv);

    // 回收资源
    (*env)->ReleaseStringUTFChars(env, old_path, oldPath);
    (*env)->ReleaseStringUTFChars(env, new_path, newPath);
    (*env)->ReleaseStringUTFChars(env, patch_path, patchPath);

    return ret;
}

这边也比较简单,将传入的参数放入数组中,然后调用bspatch中的main方法即可。

5、更新的逻辑判断

为了模拟得更像更新的逻辑,我加入了简单的VersionCode的判断。 老版APK的VersionCode是1 ,新版的版本VersionCode是2。如果是老版则更新。 因为更新耗时,里面包含了下载和合并。所以放在线程中。 如果合并成功,就通过Hanlder发送通知去安装新的APK。 我这里没有下载的逻辑,所以先将增量包放在了客户端sdcard的指定路径!

/**
     * 更新操作
     *
     * @param view
     */
    public void upload(View view) {
        if (APKUtils.getVersionCode(MainActivity.this, getPackageName()) >= 2) {
            //
            new AlertDialog.Builder(MainActivity.this)
                    .setMessage("已经是最新的版本了,无需更新!")
                    .setNegativeButton("确定", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                        }
                    }).create().show();
            return;
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 下载就省略。 由于我没有服务器进行下载

                // 1、拿到老的APK
                String oldFile = APKUtils.getSourceApkPath(MainActivity.this, getPackageName());

                // 2、拿增量包的路径
                String patchFile = Contants.PATCH_FILE_PATH;

                Log.i("AXE", patchFile);
                // 3、开始执行合并操作
                int ret = BsPatch.patch(oldFile, Contants.NEW_APK_PATH, patchFile);

                // 4、合并的成功和失败
                if (ret == 0) {
                    Log.i("AXE", "合并成功");
                    handler.sendEmptyMessage(0);
                } else {
                    Log.i("AXE", "合并失败");
                }
            }
        }).start();
    }
4.6、安装APK

7.0后的APK安装有很大的改变。 看“在应用间共享文件”这部分 具体实现我也不贴出来了,这里直接贴出其他人的博客链接。 http://blog.csdn.net/czhpxl007/article/details/53781464

5、增量更新的优缺点

优点:

  • 很明显,能节约流量,节省服务器开支。

缺点:

  • 客户端和服务端需要加入相应的支持。每次发布新版本,服务端都需要为以前所有的老版本生成对应的差分包,并根据客户端端请求返回对应的更新包,维护过程将会变得相对复杂。客户端需要对差分包做更为详细的验证,防止出错,除此之外,客户端应该可以根据服务端更新开关来确定当前是使用完整更新还是增量更新。
  • apk包之间的差异过小时,比如2m以下,此时生成的差分包仍然有几百k,此时使用增量更新得不偿失,毕竟形成差分包和合并的过程都非常耗时。另外,但版本之间变化非常大的时候,通常是是大版本好变化的时候,比如从v 1.0.0到2.0.0,此时使用完整更新也不错。

以上缺点引用CSDN博客:

http://blog.csdn.net/dd864140130/article/details/52928419 作者:### 江湖人称小白哥

最后

本文讲解了增量更新的基本逻辑、差分操作(Windows/Linux)、合并的操作(客户端)、7.0后安装APK的改变。

客户端合并的源代码: 源码传送门

参考博客:
http://blog.csdn.net/lmj623565791/article/details/52761658 http://blog.csdn.net/dd864140130/article/details/52928419