SmartAppUpdates

Introduction: Android App 增量升级,包含前后端方案、Demo、以及 so 库,可用于商店或大体积 App 差分升级
More: Author   ReportBugs   
Tags:

介绍

你所看到的,是一个用于 Android 应用程序增量更新的库。

包括客户端、服务端两部分代码。

原理

自从 Android 4.1 开始, Google Play 引入了应用程序的增量更新功能,App 使用该升级方式,可节省约 2/3 的流量。

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.

现在国内主流的应用市场也都支持应用的增量更新了。

增量更新的原理,就是将手机上已安装 apk 与服务器端最新 apk 进行二进制对比,得到差分包,用户更新程序时,只需要下载差分包,并在本地使用差分包与已安装 apk,合成新版 apk。

例如,当前手机中已安装微博 V1,大小为 12.8MB,现在微博发布了最新版 V2,大小为 15.4MB,我们对两个版本的 apk 文件差分比对之后,发现差异只有 3M,那么用户就只需要要下载一个 3M 的差分包,使用旧版 apk 与这个差分包,合成得到一个新版本 apk,提醒用户安装即可,不需要整包下载 15.4M 的微博 V2 版 apk。

apk 文件的差分、合成,可以通过 开源的二进制比较工具 bsdiff 来实现,又因为 bsdiff 依赖 bzip2,所以我们还需要用到 bzip2

bsdiff 中,bsdiff.c 用于生成差分包,bspatch.c 用于合成文件。

弄清楚原理之后,我们想实现增量更新,共需要做 3 件事:

  • 在服务器端,生成两个版本 apk 的差分包;

  • 在手机客户端,使用已安装的 apk 与这个差分包进行合成,得到新版的微博 apk;

  • 校验新合成的 apk 文件是否完整,MD5 或 SHA1 是否正确,如正确,则引导用户安装;

过程分析

1 生成差分包

这一步需要在服务器端来实现,一般来说,每当 apk 有新版本需要提示用户升级,都需要运营人员在后台管理端上传新 apk,上传时就应该由程序生成与之前所有旧版本们与最新版的差分包。

例如: 你的 apk 已经发布了 3 个版,V1.0、V2.0、V3.0,这时候你要在后台发布 V4.0,那么,当你在服务器上传最新的 V4.0 包时,服务器端就应该立即生成以下差分包:

  1. V1.0 ——> V4.0 的差分包;
  2. V2.0 ——> V4.0 的差分包;
  3. V3.0 ——> V4.0 的差分包;

ApkPatchLibraryServer 工程即为 Java 语言实现的服务器端差分程序。

下面对 ApkPatchLibraryServer 做一些简单说明:

1.1 C 部分

ApkPatchLibraryServer/jni 中,除了以下 4 个:

com_cundong_utils_DiffUtils.c

com_cundong_utils_DiffUtils.h

com_cundong_utils_PatchUtils.c

com_cundong_utils_PatchUtils.h

jni/bzip2 目录中的文件,全部来自 bzip2 项目。

com_cundong_utils_DiffUtils.c

com_cundong_utils_DiffUtils.h

用于生成差分包。

com_cundong_utils_PatchUtils.c

com_cundong_utils_PatchUtils.h

用于合成新 apk 文件。

com_cundong_utils_DiffUtils.c 修改自 bsdiff/bsdiff.ccom_cundong_utils_PatchUtils.c修改自bsdiff/bspatch.c

我们在需要将 jni 中的 C 文件,build 输出为动态链接库,以供 Java 调用(Window 环境下生成的文件名为 libApkPatchLibraryServer.dll,Unix-like 系统下为 libApkPatchLibraryServer.so,OSX 下为 libApkPatchLibraryServer.dylib)。

Build 成功后,将该动态链接库文件,加入环境变量,供 Java 语言调用。

com_cundong_utils_DiffUtils.cJava_com_cundong_utils_DiffUtils_genDiff() 方法,用于生成差分包的:


JNIEXPORT jint JNICALL Java_com_cundong_utils_DiffUtils_genDiff(JNIEnv *env,
        jclass cls, jstring old, jstring new, jstring patch) {
    int argc = 4;
    char * argv[argc];
    argv[0] = "bsdiff";
    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("new apk = %s \n", argv[2]);
    printf("patch = %s \n", argv[3]);

    int ret = genpatch(argc, argv);

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

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

    return ret;
}

com_cundong_utils_PatchUtils.cJava_com_cundong_utils_PatchUtils_patch() 方法,用于合成新的 APK;

JNIEXPORT jint JNICALL Java_com_cundong_utils_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;
}

1.2 Java 部分

com.cundong.utils 包,为调用 C 语言的 Java 实现; com.cundong.apkdiff 包,为 apk 差分程序的 Demo; com.cundong.apkpatch 包,为 apk 合并程序的 Demo;

调用,com.cundong.utils.DiffUtilsgenDiff()方法,可以通过传入的新旧 apk 路径,得到差分包。

/**
 * 类说明:     APK Diff 工具类
 * 
 * @author     Cundong
 * @date           2013-9-6
 * @version  1.0
 */
public class DiffUtils {

    /**
     * native 方法 比较路径为 oldPath 的 apk 与 newPath 的 apk 之间差异,并生成 patch 包,存储于 patchPath
     * 
     * 返回:0,说明操作成功
     *  
     * @param oldApkPath 示例:/sdcard/old.apk
     * @param newApkPath 示例:/sdcard/new.apk
     * @param patchPath  示例:/sdcard/xx.patch
     * @return
     */
    public static native int genDiff(String oldApkPath, String newApkPath, String patchPath);
}

调用,com.cundong.utils.PatchUtilspatch()方法,可以通过旧 apk 与差分包,合成为新 apk。

/**
 * 类说明:     APK Patch 工具类
 * 
 * @author    Cundong
 * @date          2013-9-6
 * @version 1.0
 */
public class PatchUtils {

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

2.使用旧版 apk 与差分包,在客户端合成新 apk

需要在手机客户端实现,ApkPatchLibrary 工程封装了这个过程。

2.1 C 部分

同 ApkPatchLibraryServer 工程一样,ApkPatchLibrary/jni/bzip2 目录中所有文件都来自 bzip2 项目。

ApkPatchLibrary/jni/com_cundong_utils_PatchUtils.cApkPatchLibrary/jni/com_cundong_utils_PatchUtils.c实现文件的合并过程,其中com_cundong_utils_PatchUtils.c修改自bsdiff/bspatch.c

我们需要用 NDK 编译出一个 libApkPatchLibrary.so 文件,生成的 so 文件位于 libs/armeabi/ 下,其他 Android 工程便可以使用该 libApkPatchLibrary.so 文件来合成 apk(如果需要支持多种 CPU 架构需要自己配置)。

com_cundong_utils_PatchUtils.Java_com_cundong_utils_PatchUtils_patch()方法,即为生成差分包的代码:

/*
 * Class:     com_cundong_utils_PatchUtils
 * Method:    patch
 * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_cundong_utils_PatchUtils_patch(JNIEnv *env,
        jobject obj, jstring old, jstring new, jstring patch) {

    char * ch[4];
    ch[0] = "bspatch";
    ch[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
    ch[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
    ch[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));

    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "old = %s ", ch[1]);
    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "new = %s ", ch[2]);
    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "patch = %s ", ch[3]);

    int ret = applypatch(4, ch);

    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "applypatch result = %d ", ret);

    (*env)->ReleaseStringUTFChars(env, old, ch[1]);
    (*env)->ReleaseStringUTFChars(env, new, ch[2]);
    (*env)->ReleaseStringUTFChars(env, patch, ch[3]);

    return ret;
}

2.2 Java 部分

com.cundong.utils 包,为调用 C 语言的 Java 实现;

调用,com.cundong.utils.PatchUtils 中 patch()方法,可以通过旧 apk 与差分包,合成为新 apk。

/**
 * 类说明:     APK Patch 工具类
 * 
 * @author   Cundong
 * @date      2013-9-6
 * @version 1.0
 */
public class PatchUtils {

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

3.校验新合成的 apk 文件

在执行 patch 之前,需要先读取本地安装旧版本 APK 的 MD5 或 SHA1,判断当前安装的文件是否为合法版本,同样,patch 得到新包之后,也需要对它进行 MD5 或 SHA1 校验,校验失败,说明合成过程有问题。

注意事项

增量更新的前提条件,是在手机客户端能让我们读取到当前应用程序安装后的源 apk,如果获取不到源 apk,那么就无法进行增量更新了,另外,如果你的应用程序不是很大,比如只有 2、3M,那么完全没有必要使用增量更新,增量更新只适用于 apk 包比较大的情况,比如手机游戏客户端。

一些说明

  • ApkPatchLibraryServer:服务器端生成差分包工程,使用 Java 实现;

  • ApkPatchLibrary:客户端使用的 apk 合成库,用于生成 libApkPatchLibrary.so,使用 Eclipse 开发;

  • ApkPatchLibrarySample:一个 Sample,手机上安装 Weibo5.5.apk,通过与 SD 卡上预先存放的 weibo.patch 文件进行合并,得到 Weibo5.6.apk,使用 AndroidStudio 开发。

  • 二进制差分或许有更好的实现方案,如:xdelta;

另外, ApkPatchLibraryServer、ApkPatchLibrarySample 中用到的 Weibo5.5.apk,Weibo5.6.apk,以及使用 ApkPatchLibraryServer 生成的差分包(Weibo5.5.apk->Weibo5.6.apk), 都通过云盘共享了

关于我

Update

1.目前的做法只是提供了一个例子,并没有做成开源库,打算这几天改进一下,做成一个开源库,push 到 GitHub 上,开发 ing..(2014 年,8 月 31 日)

2.已经大幅度重构原代码,并将原来的 Demo 程序提取成为开源库,欢迎所有人 Watch、Star、Fork。(2014 年,9 月 2 日)

3.修改 ReadMe.md,更加清晰的说明开源库的使用,同时进一步重构代码。(2014 年,10 月 4 日晚)

4.调整 ApkPatchLibraryServer 工程目录。(2015 年,4 月 24 日)

5.上传一个演示 demo ApkPatchLibrarySample.apk。(2015-4-26)

6.ApkPatchLibrarySample 重新使用 AndroidStudio 开发,修改文件 MD5 的对比逻辑。(2015-12-22)

License

Copyright 2015 Cundong

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools