本文描述通过DexClassLoader去加载一个apk包后,再使用loadClass去加载一个类的过程。主要描述的过程有
1. 类加载的过程
2. findLoadedClass的过程
3. findClass的过程
4. 类的缓存
android中加载一个类同样是使用Java的双亲委派机制去实现的,在ClassLoader的基类中,loadClass的源码如下:
1 | Class<?> clazz = findLoadedClass(className); |
当传入一个className时,先执行当前类加载器的findLoadedClass方法,这里会调用到一个native的方法,这个方法会去当前类加载器中寻找是否已经加载过这个类,如是已经加载过,则会从缓存中返回,以下会详细过程。如果在当前类加载器中找不到,则会调用父加载器中的loadClass方法,然后还是在父类加载器中重复查找是否已经加载过这个类,如此重复,直到当前的加载器为根类加载器为止。
当加载器已经回溯到顶时,parent已经为空了,此时根类加载器会执行自己的findClass方法,去当前类加载器中对应的路径中查找是否存在这个类的二进制文件;假如不存在,则会return 一个null的clazz给子加载器,子加载器再去当前类加载器对应的查找目录中查找对应的类,这样一样回到叶子类加载器为止;当然假如有一个类加载器可以找到对应的类,则会从中途的类加载器中加载这个类,而不会回到叶子。
例: 假如存在一个三级的类加载器C -> B -> A(即C的父加载器为B,B的父加载器为A,下同),它们对应的类查找目录为c,b,a,当在C中需要生成一个叫a.b.c的类(假如存在在c中)时,执行的路径如下:
1 | C.findLoadedClass |
那么,findLoadedClass是过程是怎么样的呢?上面已经说过,findLoadedClass是用来查找一个类是否已经被加载过。在vm当中,为了加速类查找,当一个类被加载过后对应的类对象会对缓存起来。findLoadedClass最终会调用一个native方法,对应的方法在 /dalvik/vm/native/java_lang_VMClassLoader.cpp::Dalvik_java_lang_VMClassLoader_findLoadedClass中。
执行的总体流程如上图,以下以代码简述流程。
由于Dalvik_java_lang_VMClassLoader_findLoadedClass比较简单,它的主要作用是作一些基本的检查,主要的功能代码是调用了dvmLookupClass方法。
1 | // /dalvik/vm/oo/Class.cpp |
ClassMatchCriteria是一个用于存放全限定名与对应的类加载器的结构体。
gDvm.loadedClasses本质上一个HashTable,用于记录已经加载过的class的hash值以及对应的ClassMatchCriteria对象数据。这是一个全局的存储对象,与dvm共存亡。
1 | // /dalvik/vm/Hash.cpp |
首先需要明确一下的是这是Hash类的一个通用方法,即所有HashTable类型的数据都可以使用这个方法来在HashTable中是否包含对应的数据,且匹配方法可以通过参数传入,还可以选择是否将数据添加到Hash表中。
在遍历方法中存在一个存入的函数方法指针cmpFunc,它实际上指向的是Class.cpp::hashcmpClassByCrit
1 | static int hashcmpClassByCrit(const void* vclazz, const void* vcrit) |
最后的添加条件定义在Class::dvmLoaderInInitiatingList中,鉴于目前暂时未能对此段代码进行深入的分析,暂不对此作记录。但看注释认为这个方法应该是用来查看传入的类加载器是否与最顶的几个类加载器(bootstrap class loader)一致,有可能是用来”破坏“双亲委派的。
当
1 | pEntry->data != HASH_TOMBSTONE && //data的指针不指向错误的地址 |
中的条件都成立时,就表示可以在缓存中找到对应的类了,此时就可以按时序图表示的返回给ClassLoader即可。
但如果找不到,因为dvmLookupClass中给dvmHashTableLookup的参数中不要求在找不到缓存的时候添加到hash表,此时就会直接返回一个null给ClassLoader,按时序图,此时就会调用parent.loadClass的方法,然后再在父加载器中重复findLoadedClass的过程,如上。假如已经没有父加载器,则会使用vm中内置的类加载器bootstrap class loader(非ClassLoader)加载。
从上述查找已加载类对象的缓存过程可以知道,全限定名与类加载器是可以确定一个类的,即使在同一个虚拟机当中已经存在一个a.b.c的全限定名且类加载器为A的类信息,但假如此时使用一个不同于类加载器A的加载器C去加载同限定名的a.b.c,也不能匹配到A中已经加载的a.b.c类对象。
===========================================================
结束findLoadedClass后,假如都找不到缓存的信息,接下来就需要调用各级加载器的findClass来查找当前路径中是否存在合适的类二进制文件(查找是顺序是从上到下的)。以下使用android中一般使用到的DexClassLoader说明。
假如类加载器C是DexClassLoader(或其子类)的对象,则在上述过程结束后,就会执行到DexClassLoader的findClass方法(此处为方法被覆盖的情况)。
如上时序图所示,BaseDexClassLoader的findClass最终会调用到DexPathList的findClass方法。DexPathList查找的文件是在构建DexClassLoader时传入的需要加载的包的路径(从源码可以看出,可以一次加载多个包,用”:“分开即可)。
1 | public Class findClass(String name, List<Throwable> suppressed) { |
DexPathList中的findClass的些特别,它其实并不是从传入的apk或者jar中查找类的,而是从odex中查找的。element.dexFile的值是从loadDexFile方法中获取的,此处会生成odex文件。
然后就可以开始执行loadClassBinaryName方法了。接下来应该都跟《DEX文件格式分析》中的类加载部分大同小异了,以后发现有神奇的地方再进行分析。