NDK开发:增量更新原理

作者: 小村医 | 来源:发表于2019-09-18 21:48 被阅读0次

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

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

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

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

一、编写java层代码,生成头文件

  1. DiffUtils
public class DiffUtils {

    /**
     * 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);
}
  1. PatchUtils
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);
}

二、引入bsdiff和bzip2源码

image.png
  1. bzip2目录中的文件,全部来自bzip2项目。
  2. bsdiff.c、bspatch.c为bsdiff项目中的代码,需要修改一下两个文件中的main函数,修改成其他函数名,bsdiff.c 用于生成差分包,bspatch.c 用于合成文件
  3. com_binzi_jni_DiffUtils.cpp
    com_binzi_jni_DiffUtils.h
    com_binzi_jni_PatchUtils.cpp
    com_binzi_jni_PatchUtils.h
    java调用的jni接口和实现

三、实现JNI接口函数

  1. com_binzi_jni_PatchUtils.c
JNIEXPORT jint JNICALL Java_com_binzi_jni_PatchUtils_patch
        (JNIEnv *env, jclass jcls, jstring oldApkPath, jstring newApkPath, jstring patchPath) {
    int argc = 4;
    char *argv[argc];
    //第一个参数没有用到
    argv[0] = "bspatch";
    argv[1] = (char *) env->GetStringUTFChars(oldApkPath, NULL);
    argv[2] = (char *) env->GetStringUTFChars(newApkPath, NULL);
    argv[3] = (char *) env->GetStringUTFChars( patchPath, NULL);

    int ret = bspatch_main(argc, argv);

    env->ReleaseStringUTFChars(oldApkPath, argv[1]);
    env->ReleaseStringUTFChars(newApkPath, argv[2]);
    env->ReleaseStringUTFChars(patchPath, argv[3]);
    return ret;
}
  1. com_binzi_jni_DiffUtils.cpp
JNIEXPORT jint JNICALL Java_com_binzi_jni_DiffUtils_patch
        (JNIEnv *env, jclass jcls, jstring oldApkPath, jstring newApkPath, jstring patchPath) {

    int argc = 4;
    char *argv[argc];
    argv[0] = "bsdiff";
    argv[1] = (char *) env->GetStringUTFChars(oldApkPath, 0));
    argv[2] = (char *) env->GetStringUTFChars(newApkPath, 0));
    argv[3] = (char *) env->GetStringUTFChars(patchPath, 0));

    int ret = bsdiff_main(argc, argv);

    env->ReleaseStringUTFChars(oldApkPath, argv[1]);
    env->ReleaseStringUTFChars(newApkPath, argv[2]);
    env->ReleaseStringUTFChars(patchPath, argv[3]);

    return ret;
}

四、Java部分核心代码

  1. 获取已安装Apk文件的源Apk文件
 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 (NameNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }
  1. 安装Apk
    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);
    }
  1. apk 签名信息获取
public class SignUtils {

    private static final boolean DEBUG = Constants.DEBUG;

    private static final String TAG = DEBUG ? "SignUtils" : SignUtils.class.getSimpleName();

    private static String bytes2Hex(byte[] src) {
        char[] res = new char[src.length * 2];
        final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        for (int i = 0, j = 0; i < src.length; i++) {
            res[j++] = hexDigits[src[i] >>> 4 & 0x0f];
            res[j++] = hexDigits[src[i] & 0x0f];
        }

        return new String(res);
    }

    private static String getMd5ByFile(File file) {
        String value = null;
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);

            MessageDigest digester = MessageDigest.getInstance("MD5");
            byte[] bytes = new byte[8192];
            int byteCount;
            while ((byteCount = in.read(bytes)) > 0) {
                digester.update(bytes, 0, byteCount);
            }
            value = bytes2Hex(digester.digest());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return value;
    }

    /**
     * 判断文件的MD5是否为指定值
     *
     * @param file
     * @param md5
     * @return
     */
    public static boolean checkMd5(File file, String md5) {
        if (TextUtils.isEmpty(md5)) {
            throw new RuntimeException("md5 cannot be empty");
        }

        String fileMd5 = getMd5ByFile(file);

        if (DEBUG) {
            Log.d(TAG, String.format("file's md5=%s, real md5=%s", fileMd5, md5));
        }

        if (md5.equals(fileMd5)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 判断文件的MD5是否为指定值
     *
     * @param filePath
     * @param md5
     * @return
     */
    public static boolean checkMd5(String filePath, String md5) {
        return checkMd5(new File(filePath), md5);
    }
}
  1. 增量更新
String oldApkSource = ApkUtils.getSourceApkPath(mContext, Constants.TEST_PACKAGENAME);
 if (!TextUtils.isEmpty(oldApkSource)) {
    // 校验一下本地安装APK的MD5是不是和真实的MD5一致
    if (SignUtils.checkMd5(oldApkSource, mCurentRealMD5)) {
        int patchResult = PatchUtils.patch(oldApkSource, Constants.NEW_APK_PATH, Constants.PATCH_PATH);
        if (patchResult == 0) {
           if (SignUtils.checkMd5(Constants.NEW_APK_PATH, mNewRealMD5)) {
                ApkUtils.installApk(context, Constants.NEW_APK_PATH);
           }
    }
}

相关文章

  • NDK开发:增量更新原理

    现在国内主流的应用市场也都支持应用的增量更新了。 增量更新的原理,就是将手机上已安装apk与服务器端最新apk进行...

  • NDK开发基础④增量更新之客户端合并差分包

    接续上篇NDK开发基础③增量更新之服务器端生成差分包 前情提要 增量更新原理就是在服务器端使用bsdiff进行文件...

  • NDK开发—增量更新

    1、普通更新和增量更新 首先了解一下应用普通更新的逻辑(这里指不通过应用市场更新):新版本发布后将APK文件上传到...

  • Android NDK开发之旅17--NDK--Apk增量更新

    Android NDK开发之旅 目录 前言 有关APK更新的技术比较多,例如:增量更新、插件式开发、热修复、RN、...

  • 一、NDK:增量更新

    增量更新在Android开发中是一种很常见的技术。 增量更新的原理 增量更新的原理非常简单,就是将本地apk与服务...

  • NDK—增量更新

    上一节我们学习了NDK来处理文件的拆分和合并操作,那时候我们纯手工来敲C语言的代码,今天我们来用C语言代码搞搞ND...

  • NDK 增量更新

    处理流程: 将应用的旧版本Apk与新版本Apk做差分,得到差分包(更新补丁) xxx.datch; 在用户下载了...

  • Android NDK开发-APK增量更新

    概述 现在的APP更新频率非常高,apk的大小也在不断的变大。如果每次新版本的更新,都让用户去下载一个完整的apk...

  • Android高级开发:解析NDK增量更新

    最近朋友跟我分享一个技巧,就是NDK增量更新,今天给大家分享一下这个技巧; 同时,在给大家分享之前,这里推荐下我自...

  • android 增量更新

    android增量更新 android 4.1开始 google引入了应用程序的增量更新。增量更新的原理实际上是使...

网友评论

    本文标题:NDK开发:增量更新原理

    本文链接:https://www.haomeiwen.com/subject/duzquctx.html