【组件健壮性】Android Java代码热修复的原理

简介: 总结Android Java代码三种热修复方式,包括自定义ClassLoader、插桩式、底层替换,并给出原理和实施流程。

应用场景

解决的问题

  • 应用发布后出现bug,修复流程又要经过开发、测试、灰度、发布整个链路,流程周期比较长,代价比较大。
  • 比较小的改动或需要立即生效的功能,想要立即触达用户,整个链路成本比较高。

解决的范围

  • Android中Java代码的热修复


方式1:自定义ClassLoader

Java ClassLoader双亲委派模型

  • Java类的双亲委派原理:某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

protected Class loadClass(String name, boolean resolve)

       throws ClassNotFoundException

   {

       synchronized (getClassLoadingLock(name)) {

           // First, check if the class has already been loaded

           Class c = findLoadedClass(name);

           if (c == null) {

               long t0 = System.nanoTime();

               try {

                   if (parent != null) {

                       c = parent.loadClass(name, false);

                   } else {

                       c = findBootstrapClassOrNull(name);

                   }

               } catch (ClassNotFoundException e) {

               }


               if (c == null) {

                   long t1 = System.nanoTime();

                   c = findClass(name);

               }

           }

           if (resolve) {

               resolveClass(c);

           }

           return c;

       }

   }


  • Android中类的加载原理:和Java类的加载机制基本一致,Java类将代码编译成class文件,JVM加载class文件;而Android多出的一步就是将class文件转换为dex文件,通过dalvik或者Art虚拟机加载,Android也有自己的类加载器。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

class Class {

   ...

       private transient ClassLoader classLoader;

   ...

}

Android中的classloader

ClassLoader是一个抽象类,而它的具体实现类主要有:

  • BootClassLoader:用于加载Android Framework层class文件。
  • PathClassLoader:用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader:用于加载指定的dex,以及jar、zip、apk中的classes.dex

他们之间的继承关系是:

public class DexClassLoader extends BaseDexClassLoader {

   public DexClassLoader(String dexPath, String optimizedDirectory,

       String librarySearchPath, ClassLoader parent) {

       super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);

   }

}


public class PathClassLoader extends BaseDexClassLoader {

   public PathClassLoader(String dexPath, ClassLoader parent) {

       super(dexPath, null, null, parent);

   }


   public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){

        super(dexPath, null, librarySearchPath, parent);

   }

}


自定义ClassLoader

  • 思路:利用双亲委派的原理,父ClassLoader会优先加载,因此创建父ClassLoader用于加载patch类。
  • 方法: 创建自定义的ClassLoader用于加载补丁class,自定义ClassLoader继承于BaseDexClassLoader,并使DexClassLoader和PathClassLoader继承于自定义ClassLoader。这样在load class时,会优先调用自定义ClassLoader去加载类。
  • 具体代码:
  • 1. 创建自定义ClassLoader,定义class文件路径

ClassLoader customClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader())

  • 2. findClass()定义加载class方法

public DexPathList(ClassLoader definingContext, String dexPath,

           String librarySearchPath, File optimizedDirectory) {

   //.........

   // splitDexPath 实现为返回 List.add(dexPath)

   // makeDexElements 会去 List.add(dexPath) 中使用DexFile加载dex文件返回 Element数组

   this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,

                                          suppressedExceptions, definingContext);

   //.........

   

}


public Class findClass(String name, List suppressed) {

    //从element中获得代表Dex的 DexFile

   for (Element element : dexElements) {

       DexFile dex = element.dexFile;

       if (dex != null) {

           //查找class

           Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);

           if (clazz != null) {

               return clazz;

           }

       }

   }

   if (dexElementsSuppressedExceptions != null) {

       suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));

   }

   return null;

}



安全问题

  • 问题:如果两个相关联的类在不同的dex中就会报错,这个校验是因为直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED标志:

  • 解决方法:
  • 防止类被打上CLASS_ISPREVERIFIED标志,需要在patch往所有类的构造函数里面插入了一段代码:

if (ClassVerifier.PREVENT_VERIFY) {

   System.out.println(AntilazyLoad.class);

}

这样当安装apk的时候,classes.dex内的类都会引用一个其他dex中的AntilazyLoad类,这样就防止了类被打上

局限性

  • 可能需要重启应用:当app运行到一半时,所需发生变更的类已经被加载过,而在Android上无法对一个类进行卸载操作,若不重启,原来的类还存储于虚拟机中,新类无法被加载。
  • 去除CLASS_ISPREVERIFIED标志,会导致性能轻微下降。



方式2:插桩式

热修复流程

  • 插桩式与自定义ClassLoader的原理类似,但没有创建自定义ClassLoader,而是利用PathClassLoader可以按顺序加载dex文件的特点,将patch.dex插入到数组中第一个位置加载。
  • 如下图:在PathClassLoader中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得dexElements中的DexFile,查找到了Key.class则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。

Element数组操作

PathClassLoader,继承自 BaseDexClassLoader,在 BaseDexClassLoader 里,有一个 DexPathList 变量,在 DexPathList 的实现里,有一个 Element[] dexElements 变量,这里面保存了所有的 dex,下面是系统

public class PathClassLoader extends BaseDexClassLoader {

}


public class BaseDexClassLoader extends ClassLoader {

   private final DexPathList pathList;

}


final class DexPathList {

   // 保存了 dex 的列表

   private Element[] dexElements;


   public Class findClass(String name, List suppressed) {

       // 遍历 dexElements

       for (Element element : dexElements) {

           DexFile dex = element.dexFile;


           if (dex != null) {

               // 从 DexFile 中查找 Class

               Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);

               if (clazz != null) {

                   return clazz;

               }

           }

       }

       // ...

       return null;

   }

}


具体实现的方法

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

       // 创建补丁 dex 的 classloader,目的是使用其中的补丁 dexElements

       DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());

       // 获取到旧的 classloader 的 pathlist.dexElements 变量

       Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));

       // 获取到补丁 classloader 的 pathlist.dexElements 变量

       Object newDexElements = getDexElements(getPathList(dexClassLoader));

       // 将补丁 的 dexElements 插入到旧的 classloader.pathlist.dexElements 前面

       Object allDexElements = combineArray(newDexElements, baseDexElements);

   }


   private static PathClassLoader getPathClassLoader() {

       PathClassLoader pathClassLoader = (PathClassLoader) InsertDexUtils.class.getClassLoader();

       return pathClassLoader;

   }


   private static Object getDexElements(Object paramObject)

           throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {

       return Reflect.on(paramObject).get("dexElements");

   }


   private static Object getPathList(Object baseDexClassLoader)

           throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

       return Reflect.on(baseDexClassLoader).get("pathList");

   }


   private static Object combineArray(Object firstArray, Object secondArray) {

       Class localClass = firstArray.getClass().getComponentType();

       int firstArrayLength = Array.getLength(firstArray);

       int allLength = firstArrayLength + Array.getLength(secondArray);

       Object result = Array.newInstance(localClass, allLength);

       for (int k = 0; k < allLength; ++k) {

           if (k < firstArrayLength) {

               Array.set(result, k, Array.get(firstArray, k));

           } else {

               Array.set(result, k, Array.get(secondArray, k - firstArrayLength));

           }

       }

       return result;

   }

方式3:底层替换方案

基本思路

        与上述java类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。底层替换方案直接替换了方法,可以立即生效不需要重启。

注意:该方法只针对Art VM, JVM的底层数据结构与Art VM不同。

replaceMethod原理

虚拟机中类的实现

  • java 中的类,方法,变量,对应到虚拟机里的实现是 Class,ArtMethod,ArtField,主要C++代码如下

class Class: public Object {

   public:

   // ...

   // classloader 指针

   uint32_t class_loader_;

   // 数组的类型表示

   uint32_t component_type_;

   // 解析 dex 生成的缓存

   uint32_t dex_cache_;

   // interface table,保存了实现的接口方法

   uint32_t iftable_;

   // 类描述符,例如:java.lang.Class

   uint32_t name_;

   // 父类

   uint32_t super_class_;

   // virtual method table,虚方法表,指令 invoke-virtual 会用到,保存着父类方法以及子类复写或者覆盖的方法,是 java 多态的基础

   uint32_t vtable_;

   // public private

   uint32_t access_flags_;

   // 成员变量

   uint64_t ifields_;

   // 保存了所有方法,包括 static,final,virtual 方法

   uint64_t methods_;

   // 静态变量

   uint64_t sfields_;

   // class 当前的状态,加载,解析,初始化等等

   Status status_;

   static uint32_t java_lang_Class_;

};


class ArtField {

   public:

   uint32_t declaring_class_;

   uint32_t access_flags_;

   uint32_t field_dex_idx_;

   uint32_t offset_;

};


class ArtMethod {

   public:

   uint32_t declaring_class_;

   uint32_t access_flags_;

   // 方法字节码的偏移

   uint32_t dex_code_item_offset_;

   // 方法在 dex 中的 index

   uint32_t dex_method_index_;

   // 在 vtable 或者 iftable 中的 index

   uint16_t method_index_;

   // 方法的调用入口

   struct PACKED(4) PtrSizedFields {

       ArtMethod** dex_cache_resolved_methods_;

       GcRoot* dex_cache_resolved_types_;

       void* entry_point_from_jni_;

       void* entry_point_from_quick_compiled_code_;

   } ptr_sized_fields_;

};

Class 中的 iftable_,vtable_,methods_ 里面保存了所有的类方法,sfields_,ifields_ 保存了所有的成员变量。而在 ArtMethod 中,ptr_sized_fields_ 变量指向了方法的调用入口,也就是执行字节码的地方。在虚拟机内部,调用一个方法的时候,可以简单的理解为会找到 ptr_sized_fields_ 指向的位置,跳转过去执行对应的方法字节码或者机器码

Method替换原理

        每次调用方法的时候,都是通过 ArtMethod 找到方法,然后跳转到其对应的字节码/机器码位置去执行,那么我们只要更改了跳转的目标位置,那么自然方法的实现也就被改变了

实现代码

  1. 找到要被替换的旧方法和新方法,可以在java层直接通过反射获取

// 创建补丁的 ClassLoader

pluginClassLoader = DexClassLoader(pluginPath, dexOutPath.absolutePath, nativeLibDir.absolutePath, this::class.java.classLoader)

// 通过补丁 ClassLoader 加载新方法

val toMethod = pluginClassLoader.loadClass("com.zy.hotfix.native_hook.PatchNativeHookUtils").getMethod("getMsg")

// 反射获取到需要修改的旧方法

val fromMethod = nativeHookUtils.javaClass.getMethod("getMsg")

  1. 调用native的C++方法替换ArtMethod 内容

nativeHookUtils.patch(fromMethod, toMethod)

Java_com_zy_hotfix_native_1hook_NativeHookUtils_patch(JNIEnv* env, jobject clazz, jobject src, jobject dest) {

   // 获取到 java 方法对应的 ArtMethod

   art::mirror::ArtMethod* smeth =

           (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

   art::mirror::ArtMethod* dmeth =

           (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);


   reinterpret_cast(dmeth->declaring_class_)->clinit_thread_id_ =

           reinterpret_cast(smeth->declaring_class_)->clinit_thread_id_;

   reinterpret_cast(dmeth->declaring_class_)->status_ =

           static_cast(reinterpret_cast(smeth->declaring_class_)->status_ -1);

   //for reflection invoke

   reinterpret_cast(dmeth->declaring_class_)->super_class_ = 0;


   // 替换方法中的内容

   smeth->declaring_class_ = dmeth->declaring_class_;

   smeth->access_flags_ = dmeth->access_flags_  | 0x0001;

   smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;

   smeth->dex_method_index_ = dmeth->dex_method_index_;

   smeth->method_index_ = dmeth->method_index_;

   smeth->hotness_count_ = dmeth->hotness_count_;

   // 替换方法的入口

   smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =

           dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;

   smeth->ptr_sized_fields_.dex_cache_resolved_types_ =

           dmeth->ptr_sized_fields_.dex_cache_resolved_types_;

   smeth->ptr_sized_fields_.entry_point_from_jni_ =

           dmeth->ptr_sized_fields_.entry_point_from_jni_;

   smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =

           dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

}


优缺点

优点:

  • 补丁可以实时生效,不需要重启应用

缺点:

兼容性问题:

  • 问题:每个版本的ArtMethod结构体不同,厂商可以改造结构体
  • 解决方法:对ArtMethod结构体的内存整个替换,使用memcpy(smeth,dmeth, sizeof(ArtMethod))。但sizeof是编译期决定,不能运行时获取。但一个类中的ArtMethod的内存分配是连续的,可以通过两个连续方法地址差值确定sizeof(ArtMethod)
相关文章
|
9月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
265 0
|
9月前
|
存储 缓存 安全
深入讲解 Java 并发编程核心原理与应用案例
本教程全面讲解Java并发编程,涵盖并发基础、线程安全、同步机制、并发工具类、线程池及实际应用案例,助你掌握多线程开发核心技术,提升程序性能与响应能力。
339 0
|
9月前
|
人工智能 安全 Java
Go与Java泛型原理简介
本文介绍了Go与Java泛型的实现原理。Go通过单态化为不同类型生成函数副本,提升运行效率;而Java则采用类型擦除,将泛型转为Object类型处理,保持兼容性但牺牲部分类型安全。两种机制各有优劣,适用于不同场景。
452 24
|
10月前
|
存储 缓存 Java
我们来详细讲一讲 Java NIO 底层原理
我是小假 期待与你的下一次相遇 ~
329 2
|
10月前
|
XML JSON Java
Java 反射:从原理到实战的全面解析与应用指南
本文深度解析Java反射机制,从原理到实战应用全覆盖。首先讲解反射的概念与核心原理,包括类加载过程和`Class`对象的作用;接着详细分析反射的核心API用法,如`Class`、`Constructor`、`Method`和`Field`的操作方法;最后通过动态代理和注解驱动配置解析等实战场景,帮助读者掌握反射技术的实际应用。内容翔实,适合希望深入理解Java反射机制的开发者。
825 13
|
10月前
|
算法 Java 索引
说一说 Java 并发队列原理剖析
我是小假 期待与你的下一次相遇 ~
110 1
|
10月前
|
安全 Java 编译器
JD-GUI,java反编译工具及原理: JavaDecompiler一个Java反编译器
Java Decompiler (JD-GUI) 是一款由 Pavel Kouznetsov 开发的图形化 Java 反编译工具,支持 Windows、Linux 和 Mac Os。它能将 `.class` 文件反编译为 Java 源代码,支持多文件标签浏览、高亮显示,并兼容 Java 5 及以上版本。JD-GUI 支持对整个 Jar 文件进行反编译,可跳转源码,适用于多种 JDK 和编译器。其原理基于将字节码转换为抽象语法树 (AST),再通过反编译生成代码。尽管程序可能带来安全风险,但可通过代码混淆降低可读性。最新版修复了多项识别错误并优化了内存管理。
8542 1
|
10月前
|
存储 算法 安全
Java中的对称加密算法的原理与实现
本文详细解析了Java中三种常用对称加密算法(AES、DES、3DES)的实现原理及应用。对称加密使用相同密钥进行加解密,适合数据安全传输与存储。AES作为现代标准,支持128/192/256位密钥,安全性高;DES采用56位密钥,现已不够安全;3DES通过三重加密增强安全性,但性能较低。文章提供了各算法的具体Java代码示例,便于快速上手实现加密解密操作,帮助用户根据需求选择合适的加密方案保护数据安全。
696 58
下一篇
开通oss服务