(九)Android 增量更新

版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、概述

1.什么是增量更新

对于平常的 Android 应用更新,大部分时候是在旧版本的代码上进行修改,打包发布。这时候,新旧版本间的差异是比较小的,增量更新就是我们在旧的应用版本基础上,只更新发生改变的,而不再是完全下载新的 apk,覆盖安装。

普通更新:
这里写图片描述

在这更新过程中,一般采用异步更新,旧版本 apk 正常运行,后台异步进行下载,下载完成之后进行弹框提示安装。

增量更新:
这里写图片描述

增量更新通过哈夫曼算法,计算出旧的 apk 与新的 apk 之间不一样的地方(差分),客户端进行版本更新的时候,只需要下载差分包到本地与旧的 apk 进行合并,生成新的 apk,然后进行安装。

2.增量更新优点

几年前,当时网络环境不好,流量费较贵,当发布新版本的时候,用户升级的意愿不高。为解决这个问题,谷歌提出了 Smart App Update,即增量更新(也叫做差分升级)。

现在网络环境虽然好了,但是各个应用 apk 也越来越大。目前来说,增量更新仍然是解决更新包过大的较好的方案。可能几个 G 的应用,更新包只有几百 M 甚至几十 M,这不仅大大提高更新速度,而且对于服务端来说,也很大程度上减少了流量的费用。

3.增量更新缺点

客户端和服务端都需要添加对增量更新的支持,而且正常情况下是无法保证用户在用版本,所以需要对各个旧的版本生产对应的更新包,根据用户上传的版本号进行下载。

当 apk 很小的时候,比如只有几 M,这时候增量包再小也有几百 K 或者上 M。如果使用增量更新有点得不偿失,增量更新较复杂,而且差分与合并过程较为耗时。另外,当进行大版本更新的时候,增量更新包也较大,这时候也不适合进行增量更新。

4.哈夫曼算法

这里写图片描述

apk 在文件存储中也是以二进制方式存在的,运用哈夫曼算法进行对比新旧版本的 apk 的二进制文件。如果是相同内容,只保存索引;如果不同,则保存压缩的内容和索引。

5.增量更新与热更新、插件化

热更新是使用热更新 service,只更新某些文件,比如原先有个 ClassA 的类文件,要进行更新为 ClassB 的类文件,不进行整个应用的下载,属于轻量级的。

插件化使用了 pluginManager,以功能块划分插件化进行开发,更新的时候对整个功能块进行更新,比如有个 PluginA 模块要更新为 PluginB 模块,相比热更新会更重量级一些。

相比于热更新和插件化,热更新和插件化更多的是体现要更新什么,而增量更新是要怎么更新。在热更新和插件化中仍可以使用增量更新,只更新 ClassA 与 ClassB 之间的类差分包,或者 PluginA 与 PluginB 的模块差分包。

二、差分

差分与合并主要是使用了 bsdiff/bspatch 进行实现的,需用用到依赖 bzip2,下面是这两个下载地址,分别下载两个压缩包。
bsdiff/bspatch 官网
http://www.daemonology.net/bsdiff/

bzip2
http://www.bzip.org/downloads.html

https://github.com/getlantern/lantern

附 csdn 下载链接:http://download.csdn.net/download/qq_18983205/10244386

1.生成可执行文件

在 bsdiff/bspatch 官网点击 Windows port 进行下载 bsdiff4.3-win32-src.zip,这是 win环境的包,也可点击 here 下载 Linux 环境下的包 bsdiff-4.3.tar.gz。

在 bsdiff4.3-win32-src.zip 解压出来的文件夹下,文件夹 Release 中已经有打包好的 bsdiff.exe 和 bspatch.exe 两个可执行文件,在这边我们不使用这个,自己新建 C 项目进行打包。

1.使用 Visual Studio 新建一个空项目 Diff,在 Diff 下新建两个文件夹 include 与 src 分别存放头文件和源代码。
这里写图片描述

2.同时把 bsdiff4.3-win32-src.zip 中对应的 .h 文件拷贝到 include 文件夹下,除 bspatch.cpp 外的 .c 和 .cpp 文件拷贝到 src 文件夹下。(这边只做差分,不需要 bspatch.cpp 文件)

这里写图片描述

**3.**Visual Studio 右击头文件 –> 添加 –> 现有项,选择 include 下面的两个头文件,添加到 Diff 项目中。
这里写图片描述

同样的,把 src 下的文件添加到源文件下。

4.这时候项目会报错,没有找到头文件,是因为源代码与头文件我们没有放在同一个文件夹下。

这里写图片描述

右键工程 –> 属性 –> c++ –> 附含包目录,选择 include 文件夹。
这里写图片描述

5.执行项目,报错。
这里写图片描述

严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C4996 ‘strcat’: This function or variable may be unsafe. Consider using strcat_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. Diff c:\users\zx\documents\visual studio 2015\projects\diff\diff\src\bzlib.c 1416

这是第三方库的警告,右键工程 –> 属性 –> c++ –> 命令行 添加 -D _CRT_SECURE_NO_WARNINGS
这里写图片描述

6.继续执行项目,继续报错。(这个错与上面的错误已经不一样了)
这里写图片描述
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C4996 ‘setmode’: The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name: _setmode. See online help for details. Diff c:\users\zx\documents\visual studio 2015\projects\diff\diff\src\bzlib.c 1422

这是安全检查报错,直接进行关闭安全检查。右键工程 –> 属性 –> c++ –> 常规 –> SDL检查 否。
这里写图片描述

7.继续执行项目,生成可执行文件 Diff.exe。
这里写图片描述

注:当 Visual Studio 进行平台切换的时候,需要进上面的步骤进行重新配置。

2.生成差分包

准备新旧两个应用的安装包,拷贝到 Diff.exe 所在的文件夹下。
这里写图片描述

切换到 Diff.exe 所在的目录,运行命令 Diff.exe app-old.exe app-new.apk app.patch 生成差分包,第一个参数是 Diff.exe 的路径。第二个参数是旧的 apk 的路径,第三个参数是新的 apk 的路径,第四个参数是生成的差分包的保存路径。

这里写图片描述

这边由于测试的原因,使用的新旧 apk 本身较小,所以与差分包的大小区别不是很明显。这也说明 apk 很小的时候不适合进行增量更新
这里写图片描述

3.生成库文件

上面是使用可执行文件生成差分包,但实际应用中不可能使用这种方式进行生成差分包,当有多个版本要生成差分包的时候,明显效率较低,一般是由 bsdiff 生成库文件,供后台进行 JNI 调用生成差分包。

1.这边使用的是 MyEclipse 作为后台开发,新建一个 web 项目,创建 Diff.java 类。(没有后台开发工具可以用 java 项目替代,jni 方法调起来即可)

public class Diff {
    public static native void diff(String oldPath, String newPath, String patchPath);
}

2.接着就是 JNI 的基本流程,生成头文件,拷贝到上面 Visual Studio 创建的 Diff 项目中,并进行配置。

JNI基本流程不熟的可以参考下前面笔记:(一)JNI 基本流程、数据类型 http://blog.csdn.net/qq_18983205/article/details/78958813

把头文件拷贝到 include 文件夹下。
这里写图片描述

3.修改 bsdiff.cpp 中 main 函数的函数名为 diff_main。
这里写图片描述

4.在 bsdiff.cpp 中引入头文件 com_xiaoyue_Diff.h。
这里写图片描述

5.在 bsdiff.cpp 中实现 native 的方法,调用 diff_main 这个方法。

JNIEXPORT void JNICALL Java_com_xiaoyue_Diff_diff
(JNIEnv *env, jclass jclz, jstring old_path_jst, jstring new_path_jst, jstring patch_path_jst) {

    int argc = 4;
    char *argv[4];

    char *old_path = (char *)(*env).GetStringUTFChars(old_path_jst, NULL);
    char *new_path = (char *)(*env).GetStringUTFChars(new_path_jst, NULL);
    char *patch_path = (char *)(*env).GetStringUTFChars(patch_path_jst, NULL);

    argv[0] = "Diff";
    argv[1] = old_path;
    argv[2] = new_path;
    argv[3] = patch_path;

    diff_main(argc, argv);

    (*env).ReleaseStringUTFChars(old_path_jst, old_path);
    (*env).ReleaseStringUTFChars(new_path_jst, new_path);
    (*env).ReleaseStringUTFChars(patch_path_jst, patch_path);
}

diff_main 需要两个参数,根据方法实现可知,第一个参数一定是 4,第二个参数是一个长度为 4 的字符串数组,4 个字符串参数分别是标签、旧的 apk 路径、新的 apk 路径和生成差分包的路径。

6.根据 JNI 基本流程生成 dll 库文件,拷贝到 web 项目中。(生成 dll 的时候,注意选择平台,切换平台需要重新进行配置)

这里写图片描述

4.JNI 调用生成差分

在 Diff.java 中添加加载。

Diff.java

public class Diff {

    public static native void diff(String oldPath, String newPath, String patchPath);

   static{
        System.loadLibrary("Diff");
    }
}

在 web 项目中编写 main 函数进行测试。

DiffTest .java

public class DiffTest {

    //路径不能包含中文
    public static final String OLD_APK_PATH = "E:/app/app-old.apk";

    public static final String NEW_APK_PATH = "E:/app/app-new.apk";

    public static final String PATCH_PATH = "E:/app/apk.patch";

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Diff.diff(OLD_APK_PATH, NEW_APK_PATH, PATCH_PATH);
    }
}

注:路径不能有中文,否则会乱码,需要进行处理。中文乱码处理可以参考: (三)JNI 中文乱码 http://blog.csdn.net/qq_18983205/article/details/78840507

结果:
这里写图片描述

5.Linux 下生成差分包

1.把 Linux 环境的 bsdiff/bspatch 包下的 bsdiff.c 和 bzip2 下的所有 .c 和 .h 文件拷贝到 Linux 平台下(虚拟机或百度云等都可以)。

这里写图片描述

2.命令行切换到对应文件夹下,运行命令 gcc -fPIC -shared blocksort.c decompress.c bsdiff.c randtable.c bzip2.c huffman.c compress.c bzlib.c crctable.c -o Diff.so。

这时候会报错,bsdiff.c 找不到 bzlib.h 头文件。
这里写图片描述

3.修改 bsdiff.c 文件中的 #include <bzlib.h>#include "bzlib.h"

注:由于个人采用的是虚拟机共享文件,碰见几个问题这边提一下:1.直接在 window 平台修改共享文件,并不会同步到虚拟机的共享文件。2.在 Linux 下直接进行修改共享文件,权限不够,没有试用 root 用户,修改较为复杂。最后是在 Linux 下把共享文件重新复制一份,然后进行修改文件权限。

继续编译,仍然出错,这是由于其他文件中也包含有 main 函数,而对于一个可执行文件只能有一个 main 函数。
这里写图片描述

4.修改其他 .c 文件下的 main 函数方法名(这边个人是在 main 前面加上文件名),重新编译即可。

三、合并

1.库文件的整合

应用的增量更新,合并是在安卓客户端这边来实现的。对于客户端来说,这块相对来说更重要一些。
1. Android Studio 新建 C++ 支持项目,把 cpp 下默认生成的 .cpp 文件删除,以及 MainActivity 中相关的代码去除。

2.把 Linux 环境的 bsdiff/bspatch 包下的 bspatch.c 和 bzip2 下的所有 .c 和 .h 文件拷贝到 cpp 下。

跟 Linux 下编译差分库一样,修改 bspatch.c 文件中的 #include <bzlib.h>#include "bzip2/bzlib.h",以及对 bzip2 下的 .c 文件修改 main 函数的函数名 。

这里写图片描述

3.修改编译配置文件 CMakeLists.txt。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

#添加整个目录的思路 指定一个变量 添加的时候使用变量值(my_c_path)
file(GLOB my_c_path src/main/cpp/bzip2/*.c)
add_library( # Sets the name of the library.
             BsPatch

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             ${my_c_path}
             src/main/cpp/bspatch.c)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       BsPatch

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

这边添加 bizp2 下的 .c 文件采用的是整个文件夹添加,也可以一个个进行添加。点击同步,然后 Clear Project。

4.修改 bspatch.c 文件中的 main 函数的函数名为 path_main。这时候 Rebuild Project 是可以成功的。
这里写图片描述

5.根据 JNI 基本生成一个 native 的方法 patch,调用 bspatch.c 下的方法。

JNI 基本流程不懂的可以参考下前面笔记:(一)JNI 基本流程、数据类型 http://blog.csdn.net/qq_18983205/article/details/78958813

创建 BsPatch.java 类。

public class BsPatch {


    public native static int patch(String oldPath, String newPath, String patchPath);

    static {
        System.loadLibrary("BsPatch");
    }
}

使用 javah 命令生成头文件,同时拷贝到 cpp 文件夹下。
这里写图片描述

在 bspatch.c 中引入该头文件 com_xiaoyue_bspatch_BsPatch.h,并实现 native 方法。
这里写图片描述

在 batch.c 中实现 native 方法 patch。

JNIEXPORT jint JNICALL Java_com_xiaoyue_bspatch_BsPatch_patch
        (JNIEnv *env, jclass jclz, jstring old_path_jst, jstring new_path_jst, jstring patch_path_jst) {

    int ret = -1;

    int argc = 4;
    char *argv[4];

    char *old_path = (char *)(*env)->GetStringUTFChars(env, old_path_jst, NULL);
    char *new_path = (char *)(*env)->GetStringUTFChars(env, new_path_jst, NULL);
    char *patch_path = (char *)(*env)->GetStringUTFChars(env, patch_path_jst, NULL);

    argv[0] = "Patch";
    argv[1] = old_path;
    argv[2] = new_path;
    argv[3] = patch_path;

    //成功返回 0
    ret = path_main(argc, argv);

    (*env)->ReleaseStringUTFChars(env, old_path_jst, old_path);
    (*env)->ReleaseStringUTFChars(env, new_path_jst, new_path);
    (*env)->ReleaseStringUTFChars(env, patch_path_jst, patch_path);

    return ret;
}

2.调用合并

这边简单的实现以下下载差分包进行合并,然后安装的流程,方法不是很完善。实际下载跟安装,可使用自己原有的方法,只需要在下载完成后添加异步调用合并的 native 方法即可。

安卓端代码:

Contants :

public class Contants {

    //差分文件服务器路径
    public static final String PATCH_FILE = "apk.patch";
    public static final String URL_PATCH_DOWNLOAD = "http://172.26.88.1:8080/UpdateService/" + PATCH_FILE;

    public static final String SD_CARD = Environment.getExternalStorageDirectory() + File.separator;

    //新版本apk的目录
    public static final String NEW_APK_PATH = SD_CARD + "apk_new.apk";
    //差分文件存储路径
    public static final String PATCH_FILE_PATH = SD_CARD + PATCH_FILE;
}

Contants 管理各个文件的路径。

DownLoadUtils :

public class DownLoadUtils {

    /**
     * 下载差分包
     * @param url
     * @return
     * @throws Exception
     */
    public static File download(String url){
        File file = null;
        InputStream is = null;
        FileOutputStream os = null;
        try {
            file = new File(Contants.PATCH_FILE_PATH);
            if (file.exists()) {
                file.delete();
            }
            HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setDoInput(true);
            is = conn.getInputStream();
            os = new FileOutputStream(file);
            byte[] buffer = new byte[1*1024];
            int len = 0;
            while((len = is.read(buffer)) != -1){
                Log.d("Tim", String.valueOf(len));
                os.write(buffer, 0, len);
            }
        } catch(Exception e){
            e.printStackTrace();
        }finally{
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return file;
    }
}

DownLoadUtils 是下载文件的工具类。

ApkUtils :

public class ApkUtils {

    /**
     * 获取APK版本号
     * @param context
     * @param packageName
     * @return
     */
    public static int getVersionCode (Context context, String packageName) {
        PackageManager pm = context.getPackageManager();
        try {
            PackageInfo info = pm.getPackageInfo(packageName, 0);
            Log.d("Tim","getVersionCode = "+info.versionCode);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取已安装Apk文件的源Apk文件
     * 如:/data/app/my.apk
     *
     * @param context
     * @param packageName
     * @return
     */
    public static String getSourceApkPath(Context context, String packageName) {
        if (TextUtils.isEmpty(packageName))
            return null;

        try {
            ApplicationInfo appInfo = context.getPackageManager()
                    .getApplicationInfo(packageName, 0);
            return appInfo.sourceDir;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 安装Apk
     *
     * @param context
     * @param apkPath
     */
    public static void installApk(Context context, String apkPath) {

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.parse("file://" + apkPath),
                "application/vnd.android.package-archive");

        context.startActivity(intent);
    }
}

ApkUtils 是 apk 的帮助类,主要提供获取 apk 版本号、获取已安装 apk 文件的源 apk 文件和安装 apk 三个方法,可采用自己实现的方法。

MainActivity :

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new ApkUpdateTask().execute();
            }
        });

    }

    class ApkUpdateTask extends AsyncTask<Void, Void, Boolean> {

        @Override
        protected Boolean doInBackground(Void... params) {

            Log.d(TAG,"开始下载 。。。");

            File patchFile = DownLoadUtils.download(Contants.URL_PATCH_DOWNLOAD) ;
            Log.d(TAG,"下载完成 。。。");

            String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());

            String newFile = Contants.NEW_APK_PATH;

            String patchFileString = patchFile.getAbsolutePath();

            Log.d(TAG,"开始合并");
            int ret = BsPatch.patch(oldfile, newFile, patchFileString);
            Log.d(TAG,"合并完成");

            if (ret == 0) {
                return true;
            } else {
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            if (aBoolean) {
                Log.d(TAG,"合并成功 开始安装新apk");
                ApkUtils.installApk(MainActivity.this, Contants.NEW_APK_PATH);
            }
        }
    }
}

MainActivity 中,使用了一个异步任务,简单的实现了一下下载差分包,整合与安装。

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xiaoyue.bspatch.MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第一版 old apk"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="更新"/>

</android.support.constraint.ConstraintLayout>

另外需要在 AndroidManifest.xml 中添加权限。

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

服务端
修改安卓端的界面和版本号,生成新旧两个 apk,应用前面的生成差分包代码进行生成差分包,直接放置于 web 项目的 WebRoot 目录下,部署在 tomcat 服务器即可。
这里写图片描述

注:如果没有服务器的话,直接生成差分包,把差分包拷贝到安卓对应目录下,修改代码,跳过下载这一步,直接进行差份,然后安装。

3.验证

验证差分包是否合并成功,一个是看合并后的差分包能否正常安装,另外可以通过查看 apk 安装包的 MD5 值进行对比。

在命令行输入: certutil -hashfile xxx.apk MD5 即可查看对应 apk 安装包的 MD5 值。

四、附

代码链接:http://download.csdn.net/download/qq_18983205/10244405

猜你喜欢

转载自blog.csdn.net/qq_18983205/article/details/79170530