Android应用增量更新/升级方案

@[增量更新,差分包,bsdiff/patch]

背景

随着Android app的不断迭代升级,功能越来越多,apk体积也越来越大,虽然当前移动网络环境较几年前有巨大提升,但流量资费依然不便宜,因此每次发布新版时用户升级并不是很积极,自从Android4.1开始,Google引入了应用程序的Smart App Update,即增量更新,增量更新提供了一个更好的方式将更新推送到设备,相对于全量更新而言前者只需要将变化的部分推送出去,这有助于用户更快的下载更新、节省设备电量消耗,最重要的是有效降低了应用升级时消耗的网络流量,国内小米、360应用市场已经使用了该更新机制推出了省流量更新功能。


官方说明

Smart app updates is a new feature of Google Play that introduces a better way of delivering app updates to devices. When developers publish an update, Google Play now delivers only the bits that have changed to devices, rather than the entire APK. This makes the updates much lighter-weight in most cases, so they are faster to download, save the device’s battery, and conserve bandwidth usage on users’ mobile data plan. On average, a smart app update is about 1/3 the sizeof a full APK update.
http://developer.android.com/about/versions/jelly-bean.html


实现原理

增量更新原理其实比较简单,就是通过差分算法将新旧版本进行对比将有差异的地方抽取出来生成更新补丁patch,也称之为差分包。客户端在检测到更新的时候,只需要将差分包下载到本地,然后通过合成算法将差分包与当前应用合并,生成最新安装包,在文件校验通过后执行安装即可。目前主流的差分比较算法是bsdiff/patch,来自http://www.daemonology.net/bsdiff/ ,该算法是开源的,可根据平台的不同在对应平台使用源代码进行编译集成。


编码实现

准备工具

  • bsdiff/patch源码(点击下载)
  • 由于bsdiff/patch依赖bzip2库,因此还需要下载bzip2。(点击下载)
  • Android studio配置NDK环境
  1. 打开Tools->Android->SDK Manager->SDK Tools选中LLDB和NDK,点击确认,软件会自动安装NDK。见下图:


    Android应用增量更新/升级方案_第1张图片
    enter image description here
  2. 配置环境变量,点击File->Project Structure打开设置页面,点击SDK Location选项卡设置NDK路径。


    Android应用增量更新/升级方案_第2张图片
    image.png

生成差分包

  • 编译bsdiff/patch,Mac环境编译方法如下:
  1. 解压下载的bsdiff-4.3.tar.gz
    tar -zxvf bsdiff-4.3.tar.gz
  2. 进入bsdiff-4.3目录,在终端下执行构建
    cd bsdiff-4.3
    make
    Window/linux平台可参考这篇文章 增量更新:bsdiff工具的安装和使用
  • bsdiff命令:
  1. 生成差分包:
    命令:bsdiff old.file new.file add.patch ,即old.file是旧的文件,new.file是新更改变化的文件,add.patch是这两个文件的差异文件(即差分包).
    生成差分包需要较多的内存和时间,所幸这些操作只需要在服务器后端执行。
  2. 旧文件和差分包合成新文件:
    命令:bspatch old.file createNew.file add.patch 其中createNew.file是合并后的新文件

合并差分包

  • 创建Native方法类
 public class PatchUtils {

    static PatchUtils instance;

    public static PatchUtils getInstance() {
        if (instance == null)
            instance = new PatchUtils();
        return instance;
    }

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

    /**
     * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
     * 
     * 返回:0,说明操作成功
     * 
     * @param oldApkPath
     *            示例:/sdcard/old.apk
     * @param newApkPath
     *            示例:/sdcard/new.apk
     * @param patchPath
     *            示例:/sdcard/xx.patch
     * @return
     */
    public native int patch(String oldApkPath, String newApkPath, String patchPath);
}

编译之后在工程build/intermediates/classes对应路径下生成PatchUtils.class文件,打开终端切换到该目录,输入命令行javah com.yyh.lib.bsdiff.PatchDroid(包名.类名),生成头文件com_yyh_lib_bsdiff_PatchUtils.h

  • 实现Native方法
    将上一个步骤生成的头文件拷贝到工程jni目录下,同时解压bzip2包和bspatch源码到该目录下,将bspatch.c重命名为com_yyh_lib_bsdiff_PatchUtils.c(注意命名方式为包名.类名),并在其中实现Java_com_yyh_lib_bsdiff_PatchUtils_patch方法,注意方法名一定要包含Native方法类所在的包名绝对路径,包名可以自定义。

JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_PatchUtils_patch
  (JNIEnv *env, jclass cls,
            jstring old, jstring new, jstring patch){
    int argc = 4;
    char * argv[argc];
    argv[0] = "bspatch";
    argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
    argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
    argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));

    printf("old apk = %s \n", argv[1]);
    printf("patch = %s \n", argv[3]);
    printf("new apk = %s \n", argv[2]);

    int ret = applypatch(argc, argv);

    printf("patch result = %d ", ret);

    (*env)->ReleaseStringUTFChars(env, old, argv[1]);
    (*env)->ReleaseStringUTFChars(env, new, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
    return ret;
}

编译SO模块

在jni目录下创建Android.mk文件,写入以下代码,其中LOCAL_MODULE表示SO模块名称,LOCAL_SRC_FILES表示源文件路径,用相对路径即可,不必写绝对路径,具体语法可参考:http://www.cnblogs.com/wainiwann/p/3837936.html,这里一定要注意加上这句代码APP_PLATFORM:=android-14,其中android-14与你工程的minSDKVersion一致即可,否则运行在某些低版本设备上会出现java.lang.UnsatisfiedLinkError错误。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ApkPatchLibrary
LOCAL_LDFLAGS := -Wl,--build-id
LOCAL_SRC_FILES := \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/com_yyh_lib_bsdiff_DiffUtils.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/com_yyh_lib_bsdiff_PatchUtils.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/blocksort.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/bzip2.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/bzip2recover.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/bzlib.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/compress.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/crctable.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/decompress.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/huffman.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/randtable.c \
    /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni/bzip2/readMe.txt \

LOCAL_C_INCLUDES += /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/main/jni
LOCAL_C_INCLUDES += /Users/xiayang075/Documents/项目/IncrementallyUpdate/app/src/debug/jni

include $(BUILD_SHARED_LIBRARY)
APP_PLATFORM:=android-14

在jni目录下创建Application.mk文件,复制以下代码:

APP_MODULES := libApkPatchLibrary (lib+so文件名)
APP_ABI := all

修改app module下的build.gradle文件,如下:

    ndk{
        moduleName "ApkPatchLibrary"
    }
    sourceSets {
        main {
            jni.srcDirs = [] //禁用gradle编译jni
            jniLibs.srcDirs = ['libs'] // libs为so文件所在包路径
        }
    }

推荐参考以下文章编译NDK,超级简单的Android Studio jni 实现(无需命令行)

将差分包与当前应用合成新包,注意生产上要注意对差分包、本地包以及生成后的新包做MD5文件校验,防止文件被篡改,确保最后生成新包的MD5值与全量包一致。

    private class PatchTask extends AsyncTask {

        @Override
        protected Integer doInBackground(String... params) {

            try {

                int result = PatchUtils.getInstance().patch(srcDir, destDir2, patchDir);
                if (result == 0) {
                    handler.obtainMessage(4).sendToTarget();
                    return WHAT_SUCCESS;
                } else {
                    handler.obtainMessage(5).sendToTarget();
                    return WHAT_FAIL_PATCH;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return WHAT_FAIL_PATCH;
        }

        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            loadding.setVisibility(View.GONE);
        }
    }

安装新包

注意使用chmod命令修改权限,否则在高版本Android系统上可能会报错。

    private void install(String dir) {
        String command = "chmod 777 " + dir;
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec(command); // 可执行权限
        } catch (IOException e) {
            e.printStackTrace();
        }

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

结语:

使用增量更新方式可以解决往常使用全量更新时安装包过大的问题,但其本身还有以下不足:

  • 多版本运营繁琐,当线上存在多个版本时,要给每个版本分别生成差分包;
  • 使用多渠道包时,要针对每个渠道包分别生成差分包,造成差分包非常多,难以维护;
  • patch依赖本地版本安装包完整性,如果本地文件损坏或者被篡改,就无法增量升级,只能下载全量包进行升级;
  • 使用bs diff/patch算法生成的差分包体积依然比较大,以同学会为例,新老包大小约为15M左右,修改少量代码并生成差分包体积达到了5M左右,与官方宣称的差量包体积约为全量包体积的1/3一致,但上述差分算法还有待优化的空间,如果需要对差分算法进行改进可参考HDiffPatch 和 rsync rolling等。

参考:

  • Android实现应用的增量更新\升级
  • IncrementallyUpdate
  • 增量更新:bsdiff工具的安装和使用
  • 超级简单的Android Studio jni 实现(无需命令行)
  • 国人开源的HDiffPatch差分算法
  • crsync-基于rsync rolling算法的文件增量更新.md

你可能感兴趣的:(Android应用增量更新/升级方案)