• Android热修复学习(一)


    前言

    什么是热修复?

    当我们应用上线后出现bug需要及时修复时,不用通过新发安装包,而是通过发布补丁包,在客户无感知的情况下修复bug;

    热修复执行流程

    开发侧修复bug提供补丁包给服务端,服务端将补丁包下发给客户端,客户端拿到补丁包后执行热修复相关代码进行修复;
    热修复执行流程图

    常用三方热修复框架原理浅析

    常见热修复方案比较

    热修复方案对比

    AndFix 阿里巴巴热修复方案【已停止维护】

    github地址

    AndFix

    方案流程

    在native动态替换java层的方法,通过native层hook java层的代码,从而改变执行顺序,进行修复;
    AndFix执行流程图

    Robust 美团热修复方案

    github地址

    Robust

    方案流程

    对每个函数都在编译打包阶段自动的插入一段代码,类似于代理,将方法执行的代码重定向到其他方法中;

       //编写的代码
        @Modify //改动代码后手动添加注解用于补丁包生成
        public long getIndex() {
            return 100;
        }
    
        //经过插桩后实际执行的代码
        public long getIndex() {
            if (changQuickRedirect != null) {
                return 修复的实现逻辑;
            }
            return 100;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Tinker 腾讯热修复方案

    github地址

    Tinker
    Tinker通过计算比对指定的Base Apk中的dex与修复后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新合成后的dex文件;
    Tinker热修复流程

    热修复简单实战

    在前面我们学习了插件化相关知识,了解了类加载机制原理以及实现插件化调用,其实热修复插件化实施差不多,区别主要是当进行dex合并的时候,需要将补丁包中的dex加载到base dex的前面,这样才能达到加载补丁包中class的目的。关于类加载机制原理这里就不再赘述,有需要可直接参考:Android插件化学习之初识类加载机制

    这里我们简单写个热修复相关的demo;

    • 先写个bug~~
    public class TestUtils {
       public static void test() {
           throw new IllegalArgumentException("故意写bug....");
           // Log.e(TestUtils.class.getSimpleName(), "bug 已修复...");
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 将bug修复并打包成dex文件存放到sdcard下
    public class TestUtils {
       public static void test() {
          // throw new IllegalArgumentException("故意写bug....");
            Log.e(TestUtils.class.getSimpleName(), "bug 已修复...");
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用dx命令打包成dex存放在sdcard目录下;
    修复dex存放sdcard下

    • 将修复后的dex与当前dex合并
    public class LoadUtils {
    
       public static void loadPluginDexFile(Context context) {
           try {
               Class<?> baseDexClassLoaderClazz = Class.forName("dalvik.system.BaseDexClassLoader");
               Field pathListField = baseDexClassLoaderClazz.getDeclaredField("pathList");
               pathListField.setAccessible(true);
    
               Class<?> dexPathListClazz = Class.forName("dalvik.system.DexPathList");
               Field dexElementsField = dexPathListClazz.getDeclaredField("dexElements");
               dexElementsField.setAccessible(true);
    
               //获取宿主dexElements
               ClassLoader hostClassLoader = context.getClassLoader();
               Object pathList = pathListField.get(hostClassLoader);
               Object[] hostDexElements = (Object[]) dexElementsField.get(pathList);
    
               //获取插件dexElements
               DexClassLoader pluginDexClassLoader = new DexClassLoader("/sdcard/patch.dex", context.getCacheDir().getAbsolutePath(), null, hostClassLoader);
               Object pluginPathList = pathListField.get(pluginDexClassLoader);
               Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
    
               //合并两个dex Element数组【修复插件的dex放在数组前面】
               Object[] resultDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(), hostDexElements.length + pluginDexElements.length);
               System.arraycopy(pluginDexElements, 0, resultDexElements, 0, pluginDexElements.length);
               System.arraycopy(hostDexElements, 0, resultDexElements, pluginDexElements.length, hostDexElements.length);
               //将合并的结果复制给宿主dex Elements
               dexElementsField.set(pathList, resultDexElements);
           } catch (Exception e) {
               e.printStackTrace();
               Log.e("LoadUtils", e.toString());
           }
       }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    Android N混合编译

    ART是在Android KitKat(Android4.0)进入并在Lolipop(Android 5.0)中设为默认运行环境,可以看作Dalvik 2.0;
    ART模式在Android7.0之前安装APK会采用AOT(Ahead of Time:提前编译、静态编译)预编译为机器码;
    而在Android N(Android7.0)使用混合模式的运行时,应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当于缓存)。根据类加载原理,类被加载后无法替换,因此存在无法修复的问题;

    混合编译热修复解决方案

    运行时,通过反射替换系统创建的PathClassLoader
    App image中的class是插入到PathClassLoader中的ClassTable中;假设我们完全废弃掉PathClassLoader,而是采用新建一个PathClassLoader加载后续所有的类,即可达到将缓存数据无用化的目的;具体方式可参考Tinker中的替换方式;

    总结

    热修复实现原理归根还是类加载机制,与插件化方案的不同之处在于修复dex需要放在合并后的dex数组前面,才能执行修复后的代码;本质上都是对于类加载机制的运用;

  • 相关阅读:
    ubantu安装k8s集群服务
    算法宝典2——Java版本(此系列持续更新,这篇文章目前3道)(有题目的跳转链接)(此份宝典包含了二叉树的算法题)
    纯代码方式杀死指定进程名的进程(Linux&Windows)
    综合电商商城小程序的作用是什么
    vue3 如何将页面生成 pdf 导出
    MySQL8.0中你不得不知道的索引新特性
    基于 .NET 7 的 QUIC 实现 Echo 服务
    Flink从入门到放弃—Stream API—常用算子(reduce)
    猿创征文|枚举类增强型for循环遍历
    【数据结构基础】之树的介绍,生动形象,通俗易懂,算法入门必看
  • 原文地址:https://blog.csdn.net/a734474820/article/details/126133407