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。见下图:


    enter image description here
  2. 配置环境变量,点击File->Project Structure打开设置页面,点击SDK Location选项卡设置NDK路径。


    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<String, Void, Integer> {

        @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等。

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 在前几年,整体移动网络环境相比现在差很多,加之流量费用又相对较高,因此每当我们发布新版本的时候,一些用户升级并不是...
    涅槃1992阅读 5,462评论 2 39
  • 1.概述 1.1.什么是应用增量更新 当我们要更新一个应用的时候,以前很多更新的做法是下载一个新版本去覆盖一个旧版...
    揚灵阅读 3,148评论 8 19
  • 风在四处的游走 一直捕风捉影般告白自己 我游走在城市乡村山野 寻觅并且沉思 成长的痛是炼狱的魔鬼 直到体无完肤遍体...
    营州布衣阅读 138评论 1 5
  • 消息中间件是目前互联网服务常用的技术服务。消息中间件为应用系统提供高效、灵活的消息同步和异步传输处理、存储转发、可...
    王帅199207阅读 509评论 0 2