Android虚拟机框架:类加载机制

关于做者html

郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工做,欢迎交流技术方面的问题,能够去个人Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。java

文章目录android

这篇文章咱们来聊一聊关于Android虚拟机的那些事,固然这里咱们并不须要去讲解关于虚拟机的底层细节,所讲的东西都是你们日常在开发中常常用的。例如类的加载机制、资源加载机制、APK打包流程、APK安装流程 以及Apk启动流程等。讲解这些知识是为了后续的文章《大型Android项目的工程化实践:插件化》、《大型Android项目的工程化实践:热更新》、《大型Android项目的工程化实践:模块化》等系列的文章作一个 原理铺垫。git

好了,让咱们开始吧~😁程序员

一 类文件基本结构

Class文件是一组以8位字节为基础的单位的二进制流,各个数据项按严格的顺序紧密的排列在Class文件中,中间没有任何间隔。github

这么说有点抽象,咱们先来举一个简单的小例子。🤞数组

public class TestClass {

    public int sum(int a, int b) {
        return a + b;
    }
}
复制代码

编译生成Class文件,而后使用hexdump命令查看Class文件里的内容。缓存

javac TestClass.java
hexdump TestClass.class
复制代码

Class文件内容以下所示:bash

Classfile /Users/guoxiaoxing/Github-app/android-open-source-project-analysis/demo/src/main/java/com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass.class
  Last modified 2018-1-23; size 333 bytes
  MD5 checksum 72ae3ff578aa0f97b9351522005ec274
  Compiled from "TestClass.java"
public class com.guoxiaoxing.android.framework.demo.native_framwork.vm.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass.m:I
   #3 = Class              #17            // com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
  #18 = Utf8               java/lang/Object
{
  public com.guoxiaoxing.android.framework.demo.native_framwork.vm.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 15: 0
}
SourceFile: "TestClass.java"

复制代码

Class文件十六机制内容以下所示:cookie

注:笔者用的二进制查看软件是iHex,能够去AppStore下载,Windows用户可使用WinHex。

这是一份十六进制表示的二进制流,每一个位排列紧密,都有其对应的含义,具体说来,以下所示:

注:下列表中四个段分别为 类型、名称、说明、数量

  • u4 magic 识别Class文件格式,具体值为0xCAFEBABE 1
  • u2 minor_version Class文件格式副版本号 1
  • u2 major_version Class文件格式主版本号 1
  • u2 constant_pool_count 常数表项个数 1
  • cp_info constant_pool 常数表,又称变长符号表 constant_pool_count-1
  • u2 access_flags Class的声明中使用的修改符掩码 1
  • u2 this_class 常数表索引,索引内保存类名或接口名 1
  • u2 super_class 常数表索引,索引内保存父类名 1
  • u2 interfaces_count 超接口个数 1
  • u2 interfaces 常数表索引,各超接口名称 interfaces_count
  • u2 fields_count 类的域个数 1
  • field_info fields 域数据,包括属性名称索引 fields_count
  • u2 methods_count 方法个数 1
  • method_info methods 方法数据,包括方法名称索引 methods_count
  • u2 attributes_count 类附加属性个数 1
  • attribute_info attributes 类附加属性数据,包括源文件名称等 attributs_count

咱们能够看着在上面这张表中有相似u二、attribute_info这样的类型,事实上Class文件采用一种相似于C语言结构体的伪结构struct来存储数据,这种结构有两种数据类型:

  • 无符号数:基本数据类型,例如u1表明1个字节,u2表明2个字节,u4表明2个字节,u8表明8个字节。
  • 表:由多个无符号数或者其余表做为数据项而构成的复合数据结构,用于描述有层次关系的复合数据结构,通常以"_info"结尾。

咱们分别来看看上述的各个字段的具体含义已经对应数值。

注:这一块的内容可能有点枯燥,可是它是咱们后续学习类加载机制,Android打包机制,以及学习插件化、热更新框架的基础,因此须要掌握。 可是也不必都记住每一个段的含义,你只须要有个总体性的认识便可,后续若是忘了具体的内容,能够再回来查阅。😁

1.1 魔数

具体含义

魔数:1-4字节,用来肯定这个文件是否为一个能被虚拟机接受的Class文件,它的值为0xCAFEBABE。

对应数值

ca fe ba be

1.2 版本号

具体含义

版本号:5-6字节是次版本号,7-8字节是主版本号

对应数值

5-6字节是次版本号0x0000(即0),7-8字节是主版本号0x0034(即52).

JDK版本号与数值的对应关系以下所示:

  • JDK 1.8 = 52
  • JDK 1.7 = 51
  • JDK 1.6 = 50
  • JDK 1.5 = 49
  • JDK 1.4 = 48
  • JDK 1.3 = 47
  • JDK 1.2 = 46
  • JDK 1.1 = 45

1.3 常量池计数/常量池

具体含义

常量池计数:常量池中常量的数量不是固定的,所以常量池入口处会放置一项u2类型的数据,表明常量池容器计数。注意容器计数从1开始,索引为0表明不引用任何一个 常量池的项目。

对应数值

9-10字节是常量池容器计数0x0013(即19)。说明常量池里有18个常量,从1-18.

这是咱们上面用javap分析的字节码文件里的常量池里常量的个数是一直的。

举个常量池里的常量的例子🤞

它的常量值以下所示:

#17 = Utf8               com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
复制代码

常量池主要存放字面量与符号引用。

字面量包括:

  • 文本字符串
  • 声明为final的常量值等

符号引用包括:

  • 类与接口的全限定名
  • 字段的名称与描述符
  • 方法的名称与描述符

常量池里的每一个常量都用一个表来表示,表的结构以下所示:

cp_info {
    //表明常量类型
    u1 tag;
    //表明存储的常量,不一样的常量类型有不一样的结构
    u1 info[];
}
复制代码

目标一共有十四中常量类型,以下所示:

注:下表字段分别为 类型、标志(tag)、描述

  • CONSTANT_Utf8_info 1 UTF8编码的Unicode字符串
  • CONSTANT_Integer_info 3 整型字面量
  • CONSTANT_Float_info 4 浮点型字面量
  • CONSTANT_Long_info 5 长整型字面量
  • CONSTANT_Double_info 6 双精度浮点型字面量
  • CONSTANT_Class_info 7 类或接口的符号引用
  • CONSTANT_String_info 8 字符串类型字面量
  • CONSTANT_Fieldref_info 9 字段的符号引用
  • CONSTANT_Methodref_info 10 类中方法的符号引用
  • CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
  • CONSTANT_NameAndType_info 12 字段或方法的部分符号引用

1.4 访问标志

具体含义

访问标志:常量池以后就是访问标志,该标志用于识别一些类或则接口层次的访问信息。这些访问信息包括这个Class是类仍是接口,是否认义Abstract类型等。

对应数值

常量池以后就是访问标志,前两个字节表明访问标志。

从上面的分析中常量池最后一个常量是#14 = Utf8 java/lang/Object,因此它后面的两个字节就表明访问标志,以下所示:

访问表示值与含义以下所示:

  • ACC_PUBLIC 0x0001 是否为public
  • ACC_FINAL 0x0010 是否为final
  • ACC_SUPER 0x0020 JDK 1.0.2之后编译出来的类该标志位都为真
  • ACC_INTERFACE 0x0200 是否为接口
  • ACC_ABSTRACT 0x0400 是否为抽象的(接口和抽象类)
  • ACC_SYNTHETIC 0x1000 表示这个代码并不是由用户产生的
  • ACC_ANNOTATION 0x2000 是否为注解
  • ACC_ENUM 0x4000 是否为枚举

咱们上面写了一个普通的Java类,ACC_PUBLIC位为真,又因为JDK 1.0.2之后编译出来的类ACC_SUPER标志位都为真,因此最终的值为:

0x0001 & 0x0020 = 0x0021
复制代码

这个值就是上图中的值。

1.5 类索引、父类索引与接口索引

具体含义

类索引(用来肯定该类的全限定名)、父类索引(用来肯定该类的父类的全限定名)是一个u2类型的数据(单个类、单继承),接口索引是一个u2类型的集合(多接口实现,用来描述该类实现了哪些接口)

对应数值

类索引、父类索引与接口索引牢牢排列在访问标志以后。

类索引为0x0002,它的全限定名为com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass。

父类索引为0x0003,它的全限定名为java/lang/Object。

接口索引的第一项是一个u2类型的数据表示接口计数器,表示实现接口的个数。这里没有实现任何接口,因此为0x0000。

1.6 字段表集合

具体含义

字段表用来描述接口或者类里声明的变量、字段。包括类级变量以及实例级变量,但不包括方法内部声明的变量。

字段表结构以下所示:

field_info {
    u2             access_flags;//访问标志位,例如private、public等
    u2             name_index;//字段的简单名称,例如int、long等
    u2             descriptor_index;//方法的描述符,描述字段的数据类型,方法的参数列表和返回值
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
复制代码

access_flags取值以下所示:

  • ACC_PUBLIC 0x0001 是否为 public;
  • ACC_PRIVATE 0x0002 是否为 private;
  • ACC_PROTECTED 0x0004 是否为 protected;
  • ACC_STATIC 0x0008 是否为 static;
  • ACC_FINAL 0x0010 是否为 final;
  • ACC_VOLATILE 0x0040 是否为 volatile;
  • ACC_TRANSIENT 0x0080 是否为 transient;
  • ACC_SYNTHETIC 0x1000 是否为 synthetic;
  • ACC_ENUM 0x4000 是否为enum.

descriptor_index里描述符的含义以下所示:

  • B byte
  • C char
  • D double
  • F float
  • I int
  • J long
  • S short
  • Z boolean
  • V void
  • L Object, 例如 Ljava/lang/Object

对应数值

  • 第一个u2类型的值为0x0001,表明当前容器计数器field_count为1,说明这个类只有一个字段表数据。也就是咱们上面定义的类成员变量private int m;
  • 第二个u2类型的值为0x0002,表明access_flags,说明这个成员变量的类型为private。
  • 第三个u2类型的值为0x0005,表明name_index为5。
  • 第四个u2类型的值为0x0006,表明descriptor_index为6。

1.7 方法表集合

方法便用来描述方法相关信息。

方法表的类型与字段表彻底相同,以下所示:

method_info {
    u2             access_flags;//访问标志位,例如private、public等
    u2             name_index;//方法名
    u2             descriptor_index;//方法的描述符,描述字段的数据类型,方法的参数列表和返回值
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
复制代码

对应的值

  • 第一个u2类型的值为0x0002,表明当前类有两个方法,即为构造函数和咱们上面写的inc()方法。
  • 第二个u2类型的值为0x0001,表明access_flags,即方法的访问类型为public。
  • 第三个u2类型的值为0x0007,表明name_index,即为。
  • 第四个u2类型的值为0x0008,表明descriptor_index,即为()V。
  • 第五个u2类型的值为0x0001,表明attributes_count,表示该方法的属性集合有一项属性。
  • 第六个u2类型的值为0x0009,表明属性名称,对应常量"code",表明此属性是方法的字节码描述。

后续还有属性表集合等相关信息,这里就再也不赘述,更多内容请参见Java虚拟机规范(Java SE 7).pdf

经过上面的描述,咱们理解了Class存储格式的细节,那么这些是如何被加载到虚拟机中去的呢,加载到虚拟机以后又会发生什么变化呢?🤔

咱们接着来看。

二 类的加载流程

什么是类的加载?🤔

类的加载就是虚拟机经过一个类的全限定名来获取描述此类的二进制字节流。

类加载的流程图以下所示:

加载

  1. 经过一个类的全限定名来获取此类的二进制流。
  2. 将这个字节流所表明的静态存储结构转换为方法去的运行时数据结构。
  3. 在内存中生成一个代码这个类的java.lang.Class对象,做为方法区这个类的各类数据的访问入口。

事实上,从哪里将一个类加载成二进制流是有很开发的,具体说来:

  • 从zip包中读取,这就发展成了咱们常见的JAR、AAR依赖。
  • 运行时动态生成,这是咱们常见的动态代理技术,在java.reflect.Proxy中就是用ProxyGenerateProxyClass来为特定接口生成代理类的二进制流。

验证

验证主要是验证加载进来的字节码二进制流是否符合虚拟机规范。

  1. 文件格式验证:验证字节码流是否符合Class文件格式的规范,而且可以被当前版本的虚拟机处理。
  2. 元数据验证:对字节码描述的语义进行分析,以保证其描述的信息符合Java语言规范的要求。
  3. 字节码验证:对字节码的数据流和控制流进行分析,肯定程序语义是合法的,符合逻辑的。
  4. 符号引用验证:这个阶段在解析阶段中完成,虚拟机将符号引用转换为直接引用。

准备

准备阶段正式为类变量分为内存并设置变量的初始值,所使用的内存在方法去里被分配,这些变量指的是被static修饰的变量,而不包括实例的变量,实例的变量会伴随着对象的实例化一块儿在Java堆 中分配。

解析

解析阶段将符号引用转换为直接引用,符号引用咱们前面已经说过,它以CONSTANT_class_info等符号来描述引用的目标,而直接引用指的是这些符号引用加载到虚拟机中之后 的内存地址。

这里的解析主要是针对咱们上面提到的字段表、方法表、属性表里面的信息,具体说来,包括如下类型:

  • 接口
  • 字段
  • 类方法
  • 接口方法
  • 方法类型
  • 方法句柄
  • 调用点限定符

初始化

初始化阶段开始执行类构造器()方法,该方法是由全部类变量的赋值动做和static语句块合并产生的

关于类构造器()方法,它和实例构造器()是不一样的,关于这个方法咱们须要注意如下几点:

  • 类构造器()方法与实例构造器()方法不一样,不须要显式的调用父类的构造器,虚拟机会保证父类构造器先执行。
  • 类构造器()方法对于类或者接口不是必须的,若是一个类既没有赋值操做,也没有静态语句块,则不会生成该方法。
  • 接口能够有变量初始化的赋值操做,所以接口也能够生成clinit>()方法、
  • 虚拟机会保证一个类的()方法在多线程环境下可以被正确的加锁和同步。若是多个线程同时去初始化一个类,那么只会有一个线程执行该类的clinit>()方法 ,其余线程会被阻塞。

讲完了类的加载流程,咱们接着来看看类加载器。

三 类加载器

3.1 Java虚拟机类加载机制

类的加载就是虚拟机经过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动做的就是类加载器。

类和类加载器息息相关,断定两个类是否相等,只有在这两个类被同一个类加载器加载的状况下才有意义,不然即使是两个类来自同一个Class文件,被不一样类加载器加载,它们也是不相等的。

注:这里的相等性保函Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果以及Instance关键字对对象所属关系的断定结果等。

类加载器能够分为三类:

  • 启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录下或者被-Xbootclasspath参数所指定的路径的,而且是被虚拟机所识别的库到内存中。
  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录下或者被java.ext.dirs系统变量所指定的路径的全部类库到内存中。
  • 应用类加载器(Application ClassLoader):负责加载用户类路径上的指定类库,若是应用程序中没有实现本身的类加载器,通常就是这个类加载器去加载应用程序中的类库。

这么多类加载器,那么当类在加载的时候会使用哪一个加载器呢?🤔

这个时候就要提到类加载器的双亲委派模型,流程图以下所示:

双亲委派模型的整个工做流程很是的简单,以下所示:

若是一个类加载器收到了加载类的请求,它不会本身当即去加载类,它会先去请求父类加载器,每一个层次的类加载器都是如此。层层传递,直到传递到最高层的类加载器,只有当 父类加载器反馈本身没法加载这个类,才会有当前子类加载器去加载该类。

关于双亲委派机制,在ClassLoader源码里也能够看出,以下所示:

public abstract class ClassLoader {
    
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            //首先,检查该类是否已经被加载
            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) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //若是父类加载器没有加载到该类,则本身去执行加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }
}
复制代码

为何要这么作呢?🤔

这是为了要让越基础的类由越高层的类加载器加载,例如Object类,不管哪一个类加载器去尝试加载这个类,最终都会传递给最高层的类加载器去加载,前面咱们也说过,类的相等性是由 类与其类加载器共同断定的,这样Object类不管在何种类加载器环境下都是同一个类。

相反若是没有双亲委派模型,那么每一个类加载器都会去加载Object,那么系统中就会出现多个不一样的Object类了,如此一来系统的最基础的行为也就没法保证了。

理解了JVM上的类加载机制,咱们再来看看Android虚拟机上上是如何加载类的。

3.2 Android虚拟机类加载机制

Java虚拟机加载的是class文件,而Android虚拟机加载的是dex文件(多个class文件合并而成),因此二者既有类似的地方,也有所不一样。

Android类加载器类图以下所示:

能够看到Android类加载器的基类是BaseDexClassLoader,它有派生出两个子类加载器:

  • PathClassLoader: 主要用于系统和app的类加载器,其中optimizedDirectory为null, 采用默认目录/data/dalvik-cache/
  • DexClassLoader: 能够从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载, 但必须是app私有可写目录来缓存odex文件. 可以加载系统没有安装的apk或者jar文件, 所以不少插件化方案都是采用DexClassLoader;

除了这两个子类觉得,还有两个类:

  • DexPathList:就跟它的名字那样,该类主要用来查找Dex、SO库的路径,并这些路径总体呈一个数组。
  • DexFile:用来描述Dex文件,Dex的加载以及Class额查找都是由该类调用它的native方法完成的。

咱们先来看看基类BaseDexClassLoader的构造方法

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
复制代码

BaseDexClassLoader构造方法的四个参数的含义以下:

  • dexPath:指的是在Androdi包含类和资源的jar/apk类型的文件集合,指的是包含dex文件。多个文件用“:”分隔开,用代码就是File.pathSeparator。
  • optimizedDirectory:指的是odex优化文件存放的路径,能够为null,那么就采用默认的系统路径。
  • libraryPath:指的是native库文件存放目录,也是以“:”分隔。
  • parent:parent类加载器

DexClassLoader与PathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了本身的构造函数,没有额外的实现,咱们对比下它们的构造函数的区别。

PathClassLoader

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);
    }
}
复制代码

DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
    
   public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}
复制代码

能够发现这两个类的构造函数最大的差异就是DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件 的地方,之后能够利用DexClassLoader实现动态加载。

上面咱们也说过,Dex的加载以及Class额查找都是由DexFile调用它的native方法完成的,咱们来看看它的实现。

咱们来看看Dex文件加载、类的查找加载的序列图,以下所示:

从上图Dex加载的流程能够看出,optimizedDirectory决定了调用哪个DexFile的构造函数。

若是optimizedDirectory为空,这个时候实际上是PathClassLoader,则调用:

DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
        throws IOException {
    this(file.getPath(), loader, elements);
}
复制代码

若是optimizedDirectory不为空,这个时候实际上是DexClassLoader,则调用:

private DexFile(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
    if (outputName != null) {
        try {
            String parent = new File(outputName).getParent();
            if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                throw new IllegalArgumentException("Optimized data directory " + parent
                        + " is not owned by the current user. Shared storage cannot protect"
                        + " your application from code injection attacks.");
            }
        } catch (ErrnoException ignored) {
            // assume we'll fail with a more contextual error later
        }
    }

    mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
    mFileName = sourceName;
    //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
复制代码

因此你能够看到DexClassLoader在加载Dex文件的时候比PathClassLoader多了一个openDexFile()方法,该方法调用的是native方法openDexFileNative()方法。

👉 dalvik_system_DexFile.cpp

这个方法并非真的打开Dex文件,而是将Dex文件以一种mmap的方式映射到虚拟机进程的地址空间中去,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,虚拟机 进程就能够采用指针的方式读写操做这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操做而没必要再调用read,write等系统调用函数。

关于mmap,它是一种颇有用的文件读写方式,限于篇幅这里再也不展开,更多关于mmap的内容能够参见文章:http://www.cnblogs.com/huxiao-tee/p/4660352.html

到这里,Android虚拟机的类加载机制就讲的差很少了,咱们再来总结一下。

Android虚拟机有两个类加载器DexClassLoader与PathClassLoader,它们都继承于BaseDexClassLoader,它们内部都维护了一个DexPathList的对象,DexPathList主要用来存放指明包含dex文件、native库和优化odex目录。 Dex文件采用DexFile这个类来描述,Dex的加载以及类的查找都是经过DexFile调用它的native方法来完成的。

相关文章
相关标签/搜索