反射机制,热更新,插件化

1:Java 文件执行的过程:
  1. 编译阶段

    • 开发者使用 .java 文件编写Java源代码。
    • 通过JDK中的 javac 编译器,.java 文件被编译成 .class 字节码文件。这一步骤是必要的,因为Java虚拟机(JVM)无法直接理解Java源代码。
  2. 为什么生成字节码

    • 字节码是一种中间代码形式,它使得Java程序能够在任何安装有JVM的平台上运行,实现“一次编写,到处运行”的目标。字节码既不依赖于源代码的高级特性,也不依赖于特定硬件平台的机器指令。
  3. 虚拟机如何识别class文件

    • 在Android环境中,.class 文件会进一步转换成 .dex (Dalvik Executable) 文件,并打包进APK文件中。
    • 当应用安装并启动时,Android运行时(ART或其前身Dalvik)会加载APK文件,并通过类加载器解析 .dex 文件里包含的类信息。这些类信息被保存在一个特定的数据结构中,称为pathList
    • pathList 的作用

      pathList是Android运行时环境中DexClassLoaderPathClassLoader内部使用的一个数据结构。它主要用于管理和组织从APK文件中加载的.dex文件。

      • 存储.dex文件:当APK被安装到设备上时,其中包含的.dex文件需要被运行时环境读取和执行。这些.dex文件首先被提取出来,并加载到运行时环境中。

      • 组织类信息:为了有效地查找和加载类定义,Android使用了名为pathList的数据结构来保存对这些已加载.dex文件的引用。每个条目(通常称为“元素”)代表一个或多个.dex文件,这些文件包含了应用程序所需执行的所有类定义。

  4. 类加载过程

    • 当执行到需要特定类(例如 Test test = new Test();)时,类加载器首先检查该类是否已经被加载到内存中。
    • 如果未加载,则通过双亲委托机制尝试找到并加载该类。双亲委托机制确保了从最顶层的类加载器开始向下查找,优先使用系统提供的类定义。
    • 类被成功加载后,在内存中创建一个 Class 对象作为该类型实例化模版。所有基于该类型创建的实例都将根据这个模版来构建。
  5. 已经加载过的情况

    • 如果请求的类已经存在于内存中(之前已经被某个类加载器加载),则直接返回对应的 Class 对象而无需重新加载。

总结

  • Java程序从源代码到执行涉及编译、打包、部署和运行等多个阶段。
  • 字节码是为了跨平台兼容性而设计的中间表示形式。
  • Android使用 .dex 格式和APK来分发和执行应用程序。
  • 类在第一次需要时被动态地加载到内存中,并且每个类型在内存里只有一个 Class 对象作为模版。
  • 双亲委托机制保证了类按照预定顺序被搜索和加载,并且系统库优先于用户定义库被使用。
 
 
2:类加载器包括pathClassLoader和dexClassLoader,他们的父加载器parent是BootClassLoader。
在8.0(API26)之前,他们二者唯一的区别是第二个参数optimizedDirectory,这个参数的意思是生成的odex(优化的dex)存放的路劲。
在8.0之后,二者完全一样,该参数已经弃用。
 
类加载时机:

1.创建对象实例的时候(new)

2.创建子类对象实例的时候,父类也会被加载

3.使用该类的静态成员变量时,该类也会加载

 
3:所有应用都是通过classLoader加载到内存的,那pathClassLoder和dexClassLoader本身也是个类,在应用启动的时候,已经被加载到内存中。
 

4.Class对象

  • 每个被成功加载并初始化的类,在JVM内部都会有一个java.lang.Class类型的对象与之对应。这个Class对象包含了该类所有相关信息,如类名、父类、接口、方法、字段等。
  • Class对象是在加载类时由JVM自动创建的。它们作为访问运行时类型信息的入口,是Java反射机制的基础。
  • 你可以通过多种方式获取到某个特定类的Class对象实例,例如:
    • 使用.class语法:比如 String.class
    • 调用对象实例上的.getClass()方法
    • 使用 Class.forName() 静态方法

示例

假设我们有一个名为 ExampleClass.java 的简单Java类:

Java
 
public class ExampleClass {

    static {

        System.out.println("ExampleClass initializing...");

    }

}

 

当我们使用以下代码请求JVM加载这个类时:

Java
 
Class<?> cls = Class.forName("ExampleClass");

 

执行流程大致如下:

  1. JVM使用系统类加载器尝试找到并读取 ExampleClass.class 文件。
  2. .class 文件被解析成运行时数据结构,并存储在方法区。
  3. 进行链接操作,包括验证、准备和解析步骤。
  4. 初始化 ExampleClass, 执行静态代码块打印输出信息。
  5. 创建代表 ExampleClassClass<?> cls 对象实例。

总结

通过上述过程,.class 文件最终被转换成了JVM可以理解和执行的格式,并且每个成功加载的类都会在JVM内部有一个相应的 Class<?> cls = Class.forName("ExampleClass");.

使用 Class.forName("ExampleClass") 这种方式来加载类可以被视为一种手动加载类的过程。这个方法在运行时动态地加载指定名称的类,并在首次使用时触发该类的初始化(比如执行静态代码块)。这与Java程序在正常执行过程中由JVM自动加载所需类的机制相区别。

 

在Java中,当我们提到Class这个词时,它可以指代两种不同的概念,这可能会造成一些混淆。以下是对这两种情况的解释:

1. 自定义类(User-defined Classes)

当你在.java文件中定义一个类时,比如:

Java
 
public class MyClass {

    // 类体

}

 

这里的MyClass是一个自定义类。它是由开发者根据需要创建的,用于封装数据和行为。在Java程序中,自定义类可以被实例化(使用new关键字创建对象),也可以包含属性(字段)、方法、构造函数等组件。

2. java.lang.Class

另一方面,java.lang.Class是Java反射API的一部分,每个自定义类在JVM内部都有一个与之对应的Class对象实例。这个对象不是开发者直接编写的代码产生的实例,而是由JVM在运行时动态创建的。它包含了与其关联的那个自定义类的元数据信息,比如类名、父类、实现的接口、拥有的方法和属性等。

你可以通过多种方式获取到某个特定类对应的Class对象实例,例如:(在这三种方法中,只有 Class.forName() 可以视作在某种程度上“手动”控制类加载和初始化过程。其他两种方法主要是基于已经存在或者预先知道的情况下获取 Class 对象)

  • 使用.class语法: Class<MyClass> cls = MyClass.class;
  • 调用对象上的.getClass()方法: Class<?> cls = myObject.getClass();
  • 使用静态方法 Class.forName()Class<?> cls = Class.forName("com.example.MyClass");

区别总结

  • 自定义类:由开发者根据业务需求直接编写并用于程序逻辑中。
  • java.lang.Class:JVM为每个加载到内存中的类型(包括自定义类、接口、数组等)动态创建并管理的一个特殊对象,主要用于反射操作。

简而言之,在Java文件里面定义的“Class”指你编写并用来构建应用程序逻辑和数据结构的代码;而当我们谈论到“一个类对应的 java.lang.Class 对象”时,则指代JVM为该类型创建和维护元数据信息所使用的内部机制。

 
4:双亲委托机制:
  应用的类加载器是PathClassLoader,sdk的类加载器是BootClassLoader
       加载类用的是双亲委托机制,一个类要被加载,先看BootClassLoader是否能够加载(BootClassLoader主要加载系统级别的类),如果无法加载,在通过pathClassLoader或者dexClassLoader加载。这样做的好处是,当我们定义一个和系统名字一样的类的时候,因为系统类前面已经被加载过了,所以不会加载我定义的这个类,就无法替换系统类。这里面涉及到一个缓存的问题,其实他的加载顺序是这样的,先查找pathClassLoader的缓存(findLoadedClass)是否之前加载过这个类,为空的话,再查找父加载器BootClassLoader的缓存,为空的话,继续查找父加载器的缓存,直到最顶层。都为空的话,开始从最顶层的父类尝试去加载这个类,如果失败,那么下一层的子类去加载,一直到最下面一层的子类。当一个类被加载之后,会被保存到相应的缓存,下次再碰到这个类的话,不需要重新加载,直接从缓存取出。
1)查看自己的缓存,是否已经加载过了
2)如果没有,让parent帮忙加载
3)如果parent加载不成功,自己加载
 
该机制的优点:
1)已经加载过得不会重复加载,从findLoadedClass里面查找
2)我们应用自定义一个和系统类一样名字的类,无法替代系统类,安全。
 
疑问:为什么父类会加载不成功,子类加载的时候可以加载到:
DexClassLoader和BootClassLoader的构造函数都有一个dexPath,存放dex文件的路径,dex文件包含class
对于BootClassLoader来说,是系统的,并不知道应用的dex,所以会加载不到,然后DexClassLoader会加载到。
 

 

 

 
5:反射机制:
  通过反射机制,我们可以获取任意一个类的属性和方法,包括private的属性和方法。
  对于private的理解:Java设计private的初衷不是为了我们绝对不能用相应的属性和方法,而是担心我们对于该类的没有把握,而希望一些属性和方法不被我们误用,是一种安全保护机制。我们通过反射获取这些private的属性和方法的前提是,我们有足够的信心保证可以正确的使用这些属性和方法。
 
6:热修复、插件化原理:
  1)所有应用的类都是通过classLoader加载到内存的,pathClassLoader里面有一个pathList,pathList里面有一个dexElements,dexElements就是dex文件存储在里面。pathClassLoader的loadclass加载的过程就是遍历dexElements里面的dex,dex其实就是.class文件,然后加载这些.class文件。
     2)热修复就是把我们修改好后的apk,封装成新的dexElements,通过反射机制,替换掉原来的dexElements,那么这样加载器在加载的时候,加载到的那个class是我们修复后的class,达到修复的目的。
     3)热修复的操作最好是放在application里面,这样程序一启动就会去加载修复的类,不这样做的问题是:
如果放在其他地方,在程序执行到这个地方的时候,原先错误的那个类可能已经被加载了,已经被存到缓存里面了。当你重启应用,执行的永远都是错误的那个类,你的修复类都不会被执行到。原理就是上面说的,会先去找缓存看是否加载过该类。

 

 
 
 
posted @ 2022-03-25 08:56  蜗牛攀爬  阅读(165)  评论(0编辑  收藏  举报