Hook之一种简单的SDK热修复方案

现状

安卓应用属于客户端程序,整个程序硬加载到用户设备中,所以安卓程序的升级在传统方法中是依赖用户的主动意识的。就意味着假如客户端出现了问题,用户是无法及时得到修复的。为了解决这个问题,便出现了很多热修复方案以及热更新框架,热更新相对原生升级策略具备很强的灵活性。这些技术同样适用于SDK。

SDK原生升级方案存在的问题

  1. 在传统升级策略中,SDK的升级严重依赖APP版本发布
  2. APP的更新依赖用户是否进行更新
  3. 无法及时对问题进行修复

目的

  1. 无缝衔接的热更新策略
  2. 实现客户端SDK的问题实时修复(一般指重新启动应用)
  3. java代码实时加载
  4. so实时加载

SDK热更新面临的问题

首次加载问题

  • 如何处理外部加载的so和jar?
    因为核心业务逻辑需要进行下发,实现热加载,那么在首次安装的时候,对于业务模块的加载需要进行特殊处理
  • 如何处理so多架构加载问题?
    因为OS在加载so文件时,会优先加载对应的CPU架构,其次才是兼容的cpu架构。对于热加载so,刚好绕过了这个问题

文件校验逻辑

  • 如何对下发的文件进行安全加固和校验?
    主要涉及到业务jar文件和so文件。一般会有一个版本号校验接口,文件下载接口。需要解决的问题:文件校验,防止文件被修改;文件加载预测试,判断文件是否被篡改

运行文件切换逻辑 running.jar、build-in.jar、download.jar

  • 对于下发的文件,如何进行版本间的切换?
    考虑存在多种情况:
    下载版本 > 内置
    下载版本 < 内置
    需要控制真正应该加载的本地so和jar

安全性问题

如何对下载文件的信任度进行检查?
文件完整性校验、预加载校验

SDK热更新方案实施

接口文件

至少需要版本号、文件地址、校验码

文件拷贝

首次加载将sdk中的业务逻辑模块拷贝到应用私有目录中,类加载器只能加载应用私有目下的dex

加载文件

确定应该被加载的文件,版本号对比逻辑

下载替换

发现新版本下发文件时进行下载和本地替换

SDK项目拆分

  1. so拆分:原先的一个so拆分成common(主要负责基础功能,改动比较小)和business(主要负责业务功能,更新可能相对频繁)两个so
  2. jar拆分:原先的一个jar拆分成proxy.jar(主要是接口和调用核心业务逻辑的代码)和remote.jar两个jar,其中proxy是jar包,remote是dex包。理论上proxy包应该很小,可能只有一个接口类和一个加载dex文件的类

SDK加载时序

存储文件说明(仅以jar作说明)

  1. 内置build-in.jar、随SDK打包在assets中的jar
  2. 下载的download.jar、根据版本号检测下载到私有目录的jar
  3. 运行的running.jar、根据版本号比较,选择被加载的jar

首次打开(私有目录没有任何缓存)

  1. 拷贝assets中的dex(build-in.jar)到私有目录命名为running.jar
  2. 加载running.jar
  3. 将内置的jar版本号(联合应用版本)、running版本号存储到sp中
  4. 请求version接口,检查下发version是否大于内置version
  5. 下载下发文件,存储到私有目录download.jar,存储下发jar版本号到sp中

二次打开(私有目录中有缓存)

  1. 比较download.jar、running.jar、build-in.jar的版本
  2. download.jar > running.jar 或者build-in.jar > running.jar 则将download.jar/build-in.jar覆盖running.jar

之所以判断build-in.jar和running.jar的版本,是因为可能应用更新过,导致内置的jar版本号比缓存的高

加载dex代码片段

/**
     * init(Application app, IEventConfig config)
     * 调用初始化方法
     * @param ctx     上下文
     * @param config  配置标志
     */
    public void execInit(Application ctx, IEventConfig config) {
        if (!isRunningExists(ctx)) {
            // 获取需要加载的dex,build-in || download || null
            File loadDexFile = getToLoadDex(ctx);
            if (loadDexFile != null) {
                Log.i("wh", "running为空加载:" + loadDexFile.getAbsolutePath());
                copyAndRenameRunningDex(ctx, loadDexFile);
                SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
            } else {
                copyDexFromAssets(ctx);
                // copy from assets && save assets dex version
                Log.i("wh", "running为空加载内置");
                copyAndRenameRunningDex(ctx, new File(getBuildInDexPath(ctx)));
                SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
            }
        } else {
            int runningVersion = SharePreferenceUtil.getInt("runningVersion");
            int downloadVersion = SharePreferenceUtil.getInt("downloadVersion");
            int internalRemoteDexVersion = ProxyConfig.getInternalRemoteDexVersion();
            Log.i("wh", "内置版本号:" + internalRemoteDexVersion);
            Log.i("wh", "运行版本号:" + runningVersion);
            Log.i("wh", "下载版本号:" + downloadVersion);

            if (internalRemoteDexVersion > runningVersion) {
                if (internalRemoteDexVersion > downloadVersion) {
                    copyDexFromAssets(ctx);
                    // copy from assets && save assets dex version
                    Log.i("wh", "内置大于当前版本号,内置版本号最高,加载内置");
                    copyAndRenameRunningDex(ctx, new File(getBuildInDexPath(ctx)));
                    SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
                } else {
                    Log.i("wh", "内置大于当前版本号,下载版本号大于内置,加载下载");
                    copyAndRenameRunningDex(ctx, new File(getDownloadDexPath(ctx)));
                    SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
                }
            } else {
                if (downloadVersion > runningVersion) {
                    Log.i("wh", "内置小于等于当前版本号,下载版本号大于当前,加载下载");
                    copyAndRenameRunningDex(ctx, new File(getDownloadDexPath(ctx)));
                    SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
                }
            }
        }
        if (isRunningExists(ctx)) {
            if (sInstance == null) {
                File runningDex = new File(getRunningDexPath(ctx));
                loadDexFromFile(ctx, runningDex);
            }
            try {
                 // 调用dex中的入口API
                if (sInstance != null) {
                    Method method = sInstance.getMethod("init", Application.class, IEventConfig.class, String.class, String.class);
                    method.invoke(null, ctx, config);
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        //加载完毕后,检查更新
        checkUpdate();
    }

方案实施的要点说明

  1. 壳jar的抽取、壳so的抽取
  2. 初始化时,正确jar的选择逻辑
  3. 对加载异常的控制
  4. 面向接口编程
发布了36 篇原创文章 · 获赞 9 · 访问量 4887

猜你喜欢

转载自blog.csdn.net/lotty_wh/article/details/103724086