Android增量更新原理和实践

demo地址
https://github.com/po1arbear/bsdiff-android

已验证过与tinker的兼容性,支持manifest修改,支持activity新增,如有其它风险和隐藏漏洞,欢迎告知 ^ ^

最近apk的更新有些频繁,各个系统发版都要向用户推送一波更新,每次都要全量下载,流量消耗大,用户等待时间长,打开小米应用商店,发现大部分app更新包都比实际的包要小,于是研究了一下,发现是使用的增量更新,了解了其原理并运用到项目中实践

一、 什么是增量更新?

首先需要明确,Android增量更新热修复不同的技术概念。

热修复一般是用于当已经发布的app有Bug需要修复的时候,开发者修改代码并发布补丁,让应用能够在不需要重新安装的情况下实现更新,主流方案有Tinker、AndFix等。

而增量更新的目的是为了减少更新app所需要下载的包体积大小,常见如手机端游戏,apk包体积为几百M,但有时更新只需下载十几M的安装包即可完成更新。

二、增量更新原理

image.png

自从 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.

三、应用市场现状

笔者使用的小米手机,可以看到,小米的应用商店已经开始支持增量更新,会比原有的方式节省超过一半的流量

image.png

四、实现方案

  • 服务端

服务端的同学拿到客户端同学开发的新版本A,跟已发布的旧版本B1,B2,B3...做了差分生成相应的差分包C1,C2,C3...,并生成相应差分包的MD5值

  • 客户端
  1. 客户端用版本号作为参数向服务端请求更新数据,若服务端没有差分包或者差分包大小比全量包大时,则返回全量包下载URL、MD5值

  2. 若服务端存在相应的差分包则返回差分包下载URL,全量包和差分包MD5值,全量包签名值和MD5值。把差分包下载到本地之后(C1),先做MD5值校验,确保下载的差分包数据的完整性,校验失败则走全量更新逻辑,校验无误和本地现有安装的旧版本(B1)进行差分合并生成新版本(A),之后进行合成版本的MD5值校验和签名校验,确保合成文件的完整性和签名信息的正确性。校验无误后再进行安装。

五、操作步骤

macOS操作验证
  1. 安装
    brew install bsdiff
  2. 准备oldfile和newfie
  3. 生成差量文件
    bsdiff oldfile newfile patchfile
  4. 合成新包
    bspatch oldfile newfile patchfile
Android上的实现

因为差量包是从接口获取的,所以客户端只需要处理bspatch的过程,合成新的apk文件然后安装即可

1.bsdiff下载

bsdiff下载后,解压bsdiff-4.3.tar.gz,取出目录中的bspatch.c文件,我们要用的就是这个文件中的bspatch_main方法。

  1. bzip2下载
    取出文件blocksort.cbzip2.cbzlib.cbzlib.hbzlib_private.hcompress.ccrctable.cdecompress.chuffman.crandtable.c,因为bsdiff的编译需要依赖bzip2,所以需要这些c文件。

  2. 将bspatch.c以及bzip的相关代码拷贝到jni目录下

image.png
  1. 编写update-lib.cpp

    
    #include 
    #include 
    #include 
    #include 
    
    #include "patchUtils.h"
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_fcbox_hivebox_update_PatchUtils_patch(JNIEnv *env, jclass type, jstring oldApkPath_,
                                               jstring newApkPath_, jstring patchPath_) {
    
        int argc = 4;
        char *ch[argc];
        ch[0] = (char *) "bspatch";
        ch[1] = const_cast(env->GetStringUTFChars(oldApkPath_, 0));
        ch[2] = const_cast(env->GetStringUTFChars(newApkPath_, 0));
        ch[3] = const_cast(env->GetStringUTFChars(patchPath_, 0));
    
    
        int ret = applypatch(argc, ch);
        __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "applypatch result = %d ", ret);
    
    
        env->ReleaseStringUTFChars(oldApkPath_, ch[1]);
        env->ReleaseStringUTFChars(newApkPath_, ch[2]);
        env->ReleaseStringUTFChars(patchPath_, ch[3]);
    
    
        return ret;
    }
    
  2. 编写PatchUtils.java

public class PatchUtils {

    // Used to load the 'native-lib' library on application startup.
    static {
      System.loadLibrary("update-lib");
    }

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

  }
  1. 调用bspatch生成新的apk
 private void genNewApk() {
    String oldpath = getApplicationInfo().sourceDir;
    String newpath = (Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
        + "composed_hivebox_apk.apk");

    String patchpath = (Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
        + "bs_patch");
    PatchUtils.patch(oldpath, newpath, patchpath);

  }

五、与Tinker的差异

  • DexDiff算法

首先简单了解下Dex文件,大家在反编译的时候,都清楚apk中会包含一个或者多个*.dex文件,该文件中存储了我们编写的代码,一般情况下我们还会通过工具转化为jar,然后通过一些工具反编译查看。

jar文件大家应该都清楚,类似于class文件的压缩包,一般情况下,我们直接解压就可以看到一个个class文件。而dex文件我们无法通过解压获取内部的一个个class文件,说明dex文件拥有自己特定的格式:

dex对JAVA类文件重新排列,将所有JAVA类文件中的常量池分解,消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池,使得相同的字符串、常量在DEX文件中只出现一次,从而减小了文件的体积。

微信通过深入Dex格式,实现一套diff差异小,内存占用少以及支持增删改的算法

  • BsDiff算法

它格式无关,但对Dex效果不是特别好,当前微信对于so与部分资源,依然使用bsdiff算法

image.png

核心思想:

  1. 将旧文件二进制使用后缀排序或哈希算法形成一个字符串索引。
  2. 使用该字符串索引对比新文件,生成差异文件(difference file)和新增文件(extra file)。
  3. 将差异文件和新增文件及必要的索引控制信息压缩为差异更新包。

你可能感兴趣的:(Android增量更新原理和实践)