You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
java.lang.LinkageError: No superclass defined for class java.lang.Object (declaration of 'java.lang.Object' appears in /data/user/0/com.sample/files/sample.dex)
目录
1 Java 类加载过程[Top]
通常,一个 Java 类被使用前,需要经过以下两个步骤:
Java 的一大魅力在于可以在运行时加载类。理论上,Java 虚拟机可以在应用程序运行过程中根据应用程序的执行要求加载任何一个合法的新类并执行。这体现了 Java 的动态性和灵活性。
我们可以试想一下 Java 类的加载过程:
首先,需要先将该类的字节码文件读到内存中;
其次,肯定需要对字节码进行一些验证,以确保它是合法并安全的,不会危害到系统,因为字节码并非只由 javac 编译 java 源程序得到,理论上只要遵循 java 字节码规范都可以得到一个虚拟机可执行的字节码文件;
然后,需要对这个类进行解析,即虚拟机需要知道这个类长什么样子,有些什么字段,需要分配多少内存,需要告诉应用程序如何才能调用到它;
最后,需要对该类进行一些必要的初始化工作,这种初始化的特点是只需要执行一次,因为类的加载一般也只需要执行一次,所以将这样的工作放到加载过程中是合理的。注意类的初始化不同于构造函数,确切地说,类的初始化是所有对象共有的部分,所以只会有一次,而构造函数应该是对象的初始化,不同的对象都要初始化一遍,所以并不属于类加载过程,从而也可以推断类的初始化应该是指的是静态语句块和静态字段的初始化。
以上是从正常的逻辑分析来看类的加载过程的,实际上,类的加载过程也大致如同上述所言,具体来看,如下图所示:
总体而言,类加载包括三个过程:加载、链接、初始化。其中,链接又包含验证、准备、解析三个阶段。一般而言,这几个过程的开始顺序是不变的,除了在 Java 动态绑定中解析在初始化之后以外。
简单介绍一下这几个过程的作用:
加载:将类的字节码二进制流读进内存;将该字节流代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
验证:如前所述,验证阶段确实就是为了验证字节码的合法性和安全性。理论上用 javac 编译过后的字节码都应该符合虚拟机规范了,所以验证过程实际上可能重复做了很多在编译阶段已经做过的事情,但是由于字节码来源的多样性,这个验证阶段还是很重要的。验证过程包括验证字节码的格式、验证类的语义合法性、方法的语义合法性、代码的语义合法性等。
准备:准备阶段比较简单,就是给类的静态变量分配内存和赋初值。需要注意的是这里不同于类的初始化阶段给静态变量赋初值,准备阶段给静态变量赋的初值一般是零值,除非该静态变量用 final 进行修饰。
解析:解析阶段将常量池内的符号引用替换为直接引用。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;直接引用则是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。符号引用与直接引用的关系就好比姓名和身份证之间的关系一样。
初始化:如前所述,初始化阶段就是根据类的逻辑语义去初始化类中的所有静态语句块和静态变量。需要注意的是,初始化的顺序将按照各个静态语句在代码中的顺序进行,特别地,静态语句块可以对在其后的静态变量重新赋值,但是不能引用它;虚拟机会保证父类的类初始化优先于子类的类初始化;类初始化是线程安全的,所以当多个线程同时初始化一个类时,若类的初始化过程太长,另一个线程将长久处在等待锁的状态。
接下来重点分析一下在应用程序端可控的第一个过程——加载。我们约定,后文提及的类加载默认情况下都指的是类加载的第一个过程——加载。
2 类加载的双亲委派模式[Top]
一般来讲,虚拟机启动后,在应用程序运行之前,都会提前预加载一些常用的基本类,比如
java.lang.*
里面的类,这些都不需要应用程序去重新加载,不光不需要,而且应该做到禁止应用程序去重新加载。因为基本类是 Java 世界里的一个统一标准套件,大家普遍遵守这个标准才能让这个世界运行有序。比如java.lang.String
这个类就是一个基本类,如果应用程序也有权限加载一个自己写的java.lang.String
,且和 JDK 的版本非常不一致,这样会让其他协作的程序员感到非常困惑,本以为这是一个 JDK 自带的字符串类型,结果发现表现出来的行为不一致。所以必须有一种可靠的类加载方案,既能让应用程序动态加载第三方的字节码,同时又能保证 Java 基本类的安全。JDK 给我们提供的这种方案叫 类加载双亲委派模式。双亲委派模式的基本原理是:
上述第 3 条是双亲委派模式的核心。可以这样理解,一个类加载器加载过的类,都不能被它的所有子孙加载器重新加载。我们以 JVM 为例,分析一下为何这样的模式可以保证基本类的安全。
先看一下 JVM 的类加载层级关系:
Bootstrap Class Loader:这是所有类加载器的祖先加载器,它负责加载 <JAVA_HOME>/lib 目录下的所有 Java 基本类,
java.lang.String
即由它加载。ExtClassLoader:扩展类加载器,负责加载 <JAVA_HOME>\lib\ext 目录中的 Java 扩展类。
AppClassLoader:应用程序类加载器,负责加载应用程序中用户类路径下的所有类,也叫系统类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。如果自定义类加载没有指定父加载器,则默认就是指定 AppClassLoader 为父加载器(由 ClassLoader.getSystemClassLoader() 指定)。
自定义类加载器:用户自定义的类加载器,一般以 AppClassLoader 作为直接父加载器,加载三方来源的字节码,如磁盘、数据库或者网络等。
上述只有 Bootstrap 加载器是以本地代码编写,应用程序无法直接使用它,剩下的类加载器都是 Java 编写的,并且都有一个共同的父类——ClassLoader,应用程序都可以直接使用。
值得注意的是,双亲委派模式的层级结构并不是由类的继承关系来实现的,而是通过组合关系来实现的。在继承关系上它们都是 ClassLoader 的子类。
由此可见,当我们用自定义的一个类加载器去加载类的时候,会回溯所有的祖先加载器,如果指定的类名与 Java 基本类或者扩展类重合,则会被 Bootstrap 或者 ExtClassLoader 加载,而不会被自定义的类加载。双亲委派的核心逻辑在 ClassLoader.loadClass() 方法中:
所以,自定义类加载器的时候,一般不要重写该方法,以免破坏双亲委派规则。但是因为历史原因,该方法是可以重写的,为了避免恶意程序通过重写该方法,直接调用 ClassLoader.defineClass() 来重新加载基本类,JDK 在 defineClass() 方法里也做了拦截验证,简单说,就是如果加载的类是以
java.*
开头的,则直接抛出SecurityException
异常。3 Android 类加载机制[Top]
3.1 Android 基本类预加载
总体来说,Android 中的类加载机制与 JVM 一样遵循双亲委派模式,在 dalvik/art 启动时就已经将所有的 Java 基本类和 Android 系统框架的基本类加载进来了。以 Android 11 为例,预加载的类记录在 /frameworks/base/config/preloaded-classes 中:
这里大概有一万多个类,这么多类当然不能每次启动一个 APP 的时候都重新加载一遍。实际上,这些类只需要在 Zygote 进程启动的时候加载一遍就可以了,后续每一个 APP 进程或者需要 Android 运行时环境的进程,都是从 Zygote 进程 fork 出来的,天然保留了加载过的类缓存。Zygote 进程预加载基本类可以参考 ZygoteInit.preload()方法:
3.2 Android 类加载器层级及各类加载器原理分析
接下来,我们看一下 Android 的类加载器层级关系:
BootClassLoader:启动类加载器,用于加载 Zygote 进程已经预加载的基本类,可以推测它只需从缓存中加载。这是基类 ClassLoader 的一个内部类,是包访问权限,所以应用程序无权直接访问。
PathClassLoader:路径类加载器,这是基类 ClassLoader 中指定的系统类加载器,由 ClassLoader.getSystemClassLoader() 返回。我们的 Android 应用程序中的所有类都是由该类加载器加载的,在初始化时显示指定了 BootClassLoader 为其父加载器。
DexClassLoader:这是 Android 应用程序用于自定义类加载器的一般父类(继承关系)。
我们知道 Android 的虚拟机(dalvik/art)与 JVM 不同,其加载的字节码文件需要打包成 .dex 文件,它与 JVM 中的字节码文件类似,但是专门针对 Android 虚拟机进行了优化。一般的流程是先通过 javac 将 java 源文件编译成 .class 文件,然后用 Android 特定的 dx 工具将多个 .class 文件打包成 .dex 文件,然后类加载器在加载类的时候,需要指定 .dex 文件的路径和在该 .dex 文件中打包的相应的类名。
所以 Android 中的类加载器在初始化时,一般都需要指定一个 .dex 文件的路径或者包含有 .dex 文件的压缩文件(.apk 文件或者 .zip 文件)的路径。也就是说,Android 加载类是在特定的字节码范围里加载的。只有一个特例,就是 BootClassLoader,它不需要指定 .dex 文件,因为它加载的类都是已经预加载过的基本类,这些基本类所属的 .dex 文件已经在 Zygote 启动时被初始化了,详细的流程可以参考 Zygote 进程启动相关源码(基本的流程是:app_main.main() -> AppRuntime.start() -> AndroidRuntime.start() -> AndroidRuntime.startVm() -> JniInvocation.JNI_CreateJavaVM(),其实现在 /art/runtime/java_vm_ext.cc 中 -> Runtime.Create() -> Runtime().Init() -> ClassLinker.InitFromBootImage(),ClassLinker.AddExtraBootDexFiles()/ClassLinker.InitWithoutImage(),最后将打开的基本类的 .dex 文件缓存在 ClassLinker 中以备加载类所用。注意,此流程之后才调用 ZygoteInit.main() -> ZygoteInit.preload(),即预加载基本类)。
我们看一下 BootClassLoader 定义的梗概:
从其 loadClass() 方法可以看到,BootClassLoader 没有父加载器,在缓存取不到类的时候直接调用自己的 findClass() 方法。
findClass() 方法很简单,简单调用 Class.classForName() 方法即可。我们注意到,在 ZygoteInit.preloadClasses() 中,加载基本类时是用的 Class.forName(),我们看一下它的实现:
可见,在预加载时,实际就是指定 BootClassLoader 作为类加载器,且只有在预加载的时候需要进行类初始化(Class.forName() 第二个参数,类初始化只需要一次即可,后续加载无需重复此过程)。
以上我们得出一个结论:通过 Class.forName() 或者 Class.classForName() 可以且仅可以直接加载基本类。这说明,一旦基本类预加载之后,对于应用程序而言,虽然不能直接访问 BootClassLoader,但却可以直接通过 Class.forName/Class.classForName 加载它们。
我们总结一下基本类的加载流程如下:
我们可以看到,无论是系统类加载器(PathClassLoader)还是自定义的类加载器(DexClassLoader),最顶层的祖先加载器默认是 BootClassLoader,与 JVM 一样,保证了基本类的类型安全。
接下来我们看一下 PathClassLoader 和 DexClassLoader 的实现原理。
PathClassLoader 和 DexClassLoader 没有本质上的区别,它们继承自一个共同的基类——BaseDexClassLoader:
所以它们只是以不同的方式执行 BaseDexClassLoader 而已。
通常情况下,PathClassLoader 是作为应用程序的系统类加载器,也是在 Zygote 进程启动的时候初始化的(基本流程为:ZygoteInit.main() -> ZygoteInit.forkSystemServer() -> ZygoteInit.handleSystemServerProcess() -> ZygoteInit.createPathClassLoader()。在预加载基本类之后执行),所以每一个 APP 进程从 Zygote 中 fork 出来之后都自动携带了一个 PathClassLoader,它通常用于加载 apk 里面的 .dex 文件。
而 DexClassLoader 则是作为应用程序自定义的类加载使用的,通常用于加载我们自己打包的 .dex 文件,这是插件化和热修复的基础。
基本的实现原理主要参考 BaseDexClassLoader。这里我们只把核心的原理阐述一下,具体的实现细节可以参考源代码实现——BaseDexClassLoader。
如下图所示为 BaseDexClassLoader 初始化和加载原理:
要点如下:
初始化中最重要的是 dex 文件的路径,准确说应该是一个包含 dex 文件的文件列表,以文件分隔符(通常为
:
)分割。文件列表中每个文件或者是一个 .dex 文件,或者是 .jar 文件,或者是 .zip 文件,或者是 .apk 文件,后面三种文件打包了 .dex 文件。所加载的类的位置与文件列表的排列顺序有关。若两个 dex 文件都包含了所加载的类,那么该类实际将从排在前面的 dex 文件中加载。这也是 Android 热修复中的一种通常做法,即通过反射将 patch dex 插入到 bug dex 的前面,从而实现了修复类对 bug 类的替换。
最终的类加载实现在本地方法 DexFile.defineClassNative() 中,这个方法与本地方法 Class.classForName() 最终都是通过本地类 ClassLinker 来实现类加载的:
DexFile.defineClassNative() 的实现在 /art/runtime/native/dalvik_system_DexFile.cc,最终由 ClassLinker.DefineClass() 实现:
Class.classForName() 的实现在 /art/runtime/native/java_lang_Class.cc
,最终由 ClassLinker.FindClass() 实现:
对于 ClassLinker 中这两个方法的实现细节我们不去关注,但是它们核心的原理可以概括为:先从已加载类的 class_table 中查询,若找到则直接返回;若找不到则说明该类是第一次加载,则执行加载流程,其中可能需要穿插加载依赖的类,加载完成后将其缓存到 class_table 中。
在 ClassLinker 中,会维护两类 class_table,一类针对基本类,一类针对其它的类。class_table 是作为缓存已经加载过的类的缓冲池。不管以什么样的方式去加载类,都需要先从 class_table 中先进行查询以提高加载性能。
我们记得在 ClassLoader.loadClass() 流程中,会先调用 findLoadedClass() 方法显示从缓存中获取类,其基本原理也是直接查询 class_table。具体实现可以参考 VMClassLoader.findLoadedClass()
前面提到,ClassLinker 在加载类的时候遇到该类依赖的类(即其继承的类或者接口)时,需要穿插加载这些依赖类,那么具体是怎么加载依赖类的呢?其实际流程是这样的:
这里的关键在 ClassLinker 对父类或继承接口执行 FindClass() 方法,即去寻找(加载)父类或继承接口。前面我们已经分析过 ClassLinker.FindClass() 和 ClassLinker.DefineClass() 两个方法的原理,它们加载类的总体流程是相似的,即先取 class_table 缓存,如果没有再执行实际的加载流程,它们之间的区别就在于实际加载流程不同,且加载父类和继承接口时,更需要两个方法进行穿插调用。
要点如下:
Android 提供的原生加载器叫做基础类加载器,包括:BootClassLoader,PathClassLoader,DexClassLoader,InMemoryDexClassLoader(Android 8.0 引入),DelegateLastClassLoader(Android 8.1 引入)。
类加载器在通过 ClassLinker.DefineClass() 加载当前类的过程中,会查找当前类的父类或继承接口,如果有父类或继承接口(除了 java.lang.Object 以外,都有父类)则需要中断当前类的加载并切换到加载父类或继承接口。
在加载父类或继承接口时,如果当前类加载器是基础加载器,则直接在 C++ 层根据相应基础加载器的规则(通常是双亲委派,但有些不是,比如 DelegateLastClassLoader)加载该父类或继承接口,这里又需要调用到 ClassLinker.DefineClass() 方法。
若当前类加载器不是基础加载器,则说明是自定义的加载器,考虑到自定义加载器可能有特定的加载规则,所以回调到 Java 层当前类加载器去加载父类或继承接口。
3.3 破坏 Android 基本类安全
我们在分析 JVM 的类加载机制的时候提到,JVM 通过双亲委派和 ClassLoader 的安全性验证来保证 Java 基本类和扩展类的类型安全。在 Android 中通常也遵循双亲委派模式加载类,但是 Android 的 ClassLoader 基类中并没有安全性验证来拦截自定义类加载器试图跳过双亲委派来加载基本类的过程。那么是不是意味着我们可以通过自定义的类加载器来加载一个与基本类同名的三方类呢?我们通过尝试一个小案例来分析一下:
第一步,我们自定义一个 java.lang.Math 类,然后改写其中的 max() 方法的功能:
第二步,用 javac 命令将该类编译成字节码文件,然后用 dx 工具将字节码文件打包成 .dex 文件。需要注意的是,用终端执行 dx 命令来打包基本类字节码会失败,这也是 Android 保证基本类安全的一种方式,所以这里我们可以用 Android Studio 在 debug 模式下打包。
第三步,写一个自定义的类加载器,用来加载这个 .dex 文件:
第四步,用第二步生成的 .dex 文件初始化 CustomClassLoader,并加载里面伪造的 java.lang.Math 类,为了验证是否加载的是伪造的类,用反射调用其中的 max() 方法:
执行之后,我们会发现打印的结果如下:
这说明仍然加载的是基本类 java.lang.Math,不然打印结果应该为
-100
。这个也好理解,因为我们自定义的 CustomClassLoader 还是严格遵循双亲委派模式的,所以实际上最终是通过 BootClassLoader 来加载了 java.lang.Math。接下来我们破坏双亲委派模式,重写 CustomClassLoader.loadClass() 方法:
即,我们不去父加载器加载,而是直接选择用当前加载器加载伪造的 java.lang.Math 类,然后再次执行以上第四步,是不是就可以直接加载这个伪造类了呢?实际结果是,我们会看到抛出 LinkageError 异常:
可以看到是因为尝试在 .dex 文件中加载 java.lang.Object 类失败。为什么会去加载 java.lang.Object 呢?根据前面对 ClassLinker.FindClass() 的分析,ClassLinker 在加载伪造的 java.lang.Math 的时候会中断去加载其父类,它的父类正好就是 java.lang.Object,同时 CustomClassLoader 也不是基础类加载器,于是必然回调到 Java 层,用 CustomClassLoader 去加载 java.lang.Object,CustomClassLoader 对 java.lang.Object 也不会走双亲委派,并且还会尝试去加载 java.lang.Object 的父类,但是 java.lang.Object 已经没有父类了,于是便抛出前述类链接错误异常。
这里有一个疑问,java.lang.Object 作为一个基本类,已经预加载了,为什么没有在 findLoadedClass() 方法中加载成功呢?这里还涉及到一个细节,为了避免大量无效查找, ClassLinker 中的 class_table 缓存是与类加载器一一对应的,即 findLoadedClass() 只会从当前类加载器(即 CustomClassLoader)对应的 class_table 中去查找缓存类,而 java.lang.Object 显然没有缓存在该 class_table 中。
既然 java.lang.Object 是一个基本类,根据前文得出的结论,只有 Class.forName()/Class.classForName() 可以加载到它,我们修改一下 CustomClassLoader.loadClass():
这样,当从 ClassLinker 回调到 CustomClassLoader.loadClass() 加载 java.lang.Object 时,我们直接用 Class.forName() 来加载它。再次执行第四步,得到打印结果:
满足执行预期。
由此我们得出一个结论:在 Android 中,可以通过破坏双亲委派的方式加载与基本类同名的类。只不过需要注意,我们给出的案例比较简单,当你真正需要加载一个与基本类同名的类时,需要对其引用的所有基本类都进行特殊处理(一种方法就是直接用 Class.forName() 来加载其引用的基本类)。而其引用的类不光指其继承的类,还包括其成员变量中、方法中引用到的所有基本类。理论上,如果想要让同名的基本类正常运行,在你的自定义类加载方法中,需要对
/frameworks/base/config/preloaded-classes
中的所有类都进行特殊处理。但是,在 JVM 中,即使破坏双亲委派模式,由于还有一层安全性验证,对基本类的保护比 Android 更严格。
The text was updated successfully, but these errors were encountered: