【组件健壮性】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)
相关文章
|
10天前
|
设计模式 Java
Java设计模式:组合模式的介绍及代码演示
组合模式是一种结构型设计模式,用于将多个对象组织成树形结构,并统一处理所有对象。例如,统计公司总人数时,可先统计各部门人数再求和。该模式包括一个通用接口、表示节点的类及其实现类。通过树形结构和节点的通用方法,组合模式使程序更易扩展和维护。
Java设计模式:组合模式的介绍及代码演示
|
1天前
|
Java API 开发者
探索Java中的Lambda表达式:简洁与强大的代码实践
本文深入探讨Java中Lambda表达式的定义、用法及优势,通过实例展示其如何简化代码、提升可读性,并强调在使用中需注意的兼容性和效率问题。Lambda作为Java 8的亮点功能,不仅优化了集合操作,还促进了函数式编程范式的应用,为开发者提供了更灵活的编码方式。
|
1天前
|
Java Linux Python
Linux环境下 代码java调用python出错
Linux环境下 代码java调用python出错
12 3
|
5天前
|
存储 开发框架 数据可视化
深入解析Android应用开发中的四大核心组件
本文将探讨Android开发中的四大核心组件——Activity、Service、BroadcastReceiver和ContentProvider。我们将深入了解每个组件的定义、作用、使用方法及它们之间的交互方式,以帮助开发者更好地理解和应用这些组件,提升Android应用开发的能力和效率。
|
2天前
|
算法 Java
java 概率抽奖代码实现
java 概率抽奖代码实现
|
5天前
|
ARouter 测试技术 API
Android经典面试题之组件化原理、优缺点、实现方法?
本文介绍了组件化在Android开发中的应用,详细阐述了其原理、优缺点及实现方式,包括模块化、接口编程、依赖注入、路由机制等内容,并提供了具体代码示例。
18 2
|
7天前
|
存储 Java Android开发
🔥Android开发大神揭秘:从菜鸟到高手,你的代码为何总是慢人一步?💻
在Android开发中,每位开发者都渴望应用响应迅速、体验流畅。然而,代码执行缓慢却是常见问题。本文将跟随一位大神的脚步,剖析三大典型案例:主线程阻塞导致卡顿、内存泄漏引发性能下降及不合理布局引起的渲染问题,并提供优化方案。通过学习这些技巧,你将能够显著提升应用性能,从新手蜕变为高手。
15 2
|
10天前
|
Java 程序员 API
Java中的Lambda表达式:简化代码的秘密武器
在Java 8中引入的Lambda表达式是一种强大的编程工具,它可以显著简化代码,提高可读性。本文将介绍Lambda表达式的基本概念、优势以及在实际开发中的应用。通过具体示例,您将了解如何使用Lambda表达式来简化集合操作、线程编程和函数式编程。让我们一起探索这一革命性的特性,看看它是如何改变Java编程方式的。
21 4
|
10天前
|
Java 开发者
探索Java中的Lambda表达式:简化你的代码
【8月更文挑战第49天】在Java 8的发布中,Lambda表达式无疑是最令人兴奋的新特性之一。它不仅为Java开发者提供了一种更加简洁、灵活的编程方式,而且还极大地提高了代码的可读性和开发效率。本文将通过实际代码示例,展示如何利用Lambda表达式优化和重构Java代码,让你的编程之旅更加轻松愉快。
|
8天前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
35 1