Android从5.0开始就采用art虚拟机, 该虚拟机有些类似JAVA虚拟机, 程序运行过程也需要通过ClassLoader 将目标类加载到内存.
传统Jvm主要是通过读取class字节码来加载, 而art则是从dex字节码来读取. 这是一种更为优化的方案, 可以将多个.class文件合并成一个classes.dex文件. 下面直接来看看ClassLoader的关系。
接下来依次看看PathClassLoader,DexClassLoader,BaseDexClassLoader,BootClassLoader,ClassLoader这5个类加载器。
PathClassLoader和DexClassLoader,它们都继承自BaseDexClassLoader,这两个类有什么区别呢?其实看一下它们的源码注释就一目了然了。因为代码很少,约等于没有,这里直接贴出它们的源码,其实主要是注释:
由注释看可以发现PathClassLoader被用来加载本地文件系统上的文件或目录,但不能从网络上加载,关键是它被用来加载系统类和我们的应用程序,这也是为什么它的两个构造函数中调用父类构造器的时候第二个参数传null,具体的参数意义请看接下来DexClassLoader的注释。
DexClassLoader用来加载jar、apk,其实还包括zip文件或者直接加载dex文件,它可以被用来执行未安装的代码或者未被应用加载过的代码。这里也写出了它需要的四个参数的意思
这里着重看一下第二个参数,之前说过PathClassLoader中调用父类构造器的时候这个参数穿了null,因为加载App应用的时候我们的apk已经被安装到本地文件系统上了,其内部的dex已经被提取并且执行过优化了,优化之后放在系统目录/data/dalvik-cache下。
BaseDexClassLoader构造函数, 有一个非常重要的过程, 那就是初始化DexPathList对象.
另外该构造函数的参数说明:
再来看看SystemClassLoader,这里的getSystemClassLoader()返回的是PathClassLoader类。
首先看一段如何使用类加载器加载的调用代码:
1 try { 2 File file = view.getActivity().getDir("dex",0); 3 String optimizedDirectory = file.getAbsolutePath(); 4 DexClassLoader loader = new DexClassLoader("需要被加载的dex文件所在的路径",optimizedDirectory,null,context.getClassLoader()); 5 loader.loadClass("需要加载的类的完全限定名"); 6 } catch (ClassNotFoundException e) { 7 e.printStackTrace(); 8 }
这里我们就用了自定义了一个DexClassLoaderLoader,并且调用了它的loadClass方法,这样一个需要被使用的类就被我们加载进来了,接下去就可以正常使用这个类了,具体怎么使用我就不多说了,我们还是来研究研究这个类是怎么被加载进来的吧~
可以看到new DexClassLoader的时候我们用了4个参数,参数意义上面已经讲过了,从上面的源码中可以看到DexClassLoader的构造器中直接调用了父类的构造器,只是将optimizedDirectory路径封装成一个File,具体这些参数是如何被使用的呢,我们往下看。
BaseDexClassLoader类的构造器
首先也是调用了父类的构造器,但这里只将parent传给父类,即ClassLoader,ClassLoader中做的也很很简单,它内部有个parent属性,正好保存传进来的参数parent,这里可以稍微看一下第二个参数的注释,最后一句说到可以为null,而是否为null又刚好是PathClassLoader和DexClassLoader的区别,那是否为null最终又意味着什么呢?
接下来BaseDexClassLoader给originalPath 和 pathList赋了值,originalPath就是我们传进入的dex文件路径,pathList 是一个new 出来的DexPathList对象。
别的先不说,先看注释。第四个参数中说到如果optimizedDirectory 为null则使用系统默认路径代替,这个默认路径也就是/data/dalvik-cache/目录,这个一般情况下是没有权限去访问的,所以这也就解释了为什么我们只能用DexClassLoader去加载类而不用PathClassLoader。
然后接着看代码,显然,前面三个if判断都是用来验证参数的合法性的,之后同样只是做了三个赋值操作,第一个就不说了,保存了实例化DexPathList的classloader,第二个参数的声明是一个Element数组,第三个参数是lib库的目录文件数组。
看它们之前先看看几个split小函数:
这两个顾名思义就是拿来分割dexPath和libPath,它们内部都调用了splitPaths方法,只是三个参数不一样,其中splitLibraryPath方法中调用splitPaths时的第二个参数仿佛又透露了什么信息,没错,之前介绍DexClassLoader参数中的libraryPath的时候说过,会加上系统so库的存放目录,就是在这个时候添加上去的。
什么啊,原来这个方法也没做什么事啊,只是把参数path1和参数path2又分别调用了下splitAndAdd方法,但是这里创建了一个ArrayList,而且调用splitAndAdd方法的时候都当参数传入了,并且最终返回了这个list,所以我们大胆猜测下,path1和path2最后被分割后的值都存放在了list中返回,看下是不是这么一回事吧:
果然,跟我们猜的一样,只是又加上了文件是否存在以及是否可读的验证,然后根据参数wantDirectories判断是否文件类型是被需要的类型,最终加入list。现在我们回过头去看看splitDexPath方法和splitLibraryPath方法,是不是一目了然了。
再往上看DexPathList的构造器,nativeLibraryDirectories的最终值也已经知道了,就差dexElements了,makeDexElements方法的两个参数我们也已经知道了,那我们就看看makeDexElements都干了些什么吧。
方法也不长,我们一段段看下去。首先创建了一个elememt 列表,然后遍历由dexpath分割得来的文件列表,其实一般使用场景下也就一个文件。循环里面针对每个file 声明一个zipfile和一个dexfile,判断file的文件后缀名,如果是".dex"则使用loadDexFile方法给dex赋值,如果是“.apk”,“.jar”,“.zip”文件的则把file包装成zipfile赋值给zip,然后同样是用loadDexFile方法给dex赋值,如果是其他情况则不做处理,打印日志说明文件类型不支持,而且下一个if判断中由于zip和dex都未曾赋值,所以也不会添加到elements列表中去。注意下:这里所谓的文件类型仅仅是指文件的后缀名而已,并不是文件的实际类型,比如我们将.zip文件后缀改成.txt,那么就不支持这个文件了。而且我们可以看到对于dexpath目前只支持“.dex”、“.jar”、“.apk”、“.zip”这四种类型。
现在还剩下两个东西可能还不太明确,一个是什么是DexFile以及这里的loadDexFile方法是如何创建dexfile实例的,另一个是什么是Elememt,看了下Element源码,哈哈,so easy,就是一个简单的数据结构体加一个方法,所以我们就先简单把它当做一个存储了file,zip,dex三个字段的一个实体类。那么就剩下DexFile了。
很简洁,如果optimizedDirectory == null则直接new 一个DexFile,否则就使用DexFile.loadDex来创建一个DexFile实例。
这个方法获取被加载的dexpath的文件名,如果不是“.dex”结尾的就改成“.dex”结尾,然后用optimizedDirectory和新的文件名构造一个File并返回该File的路径,所以DexFile.loadDex方法的第二个参数其实是dexpath文件对应的优化文件的输出路径。比如我要加载一个dexpath为“sdcard/coder_yu/plugin.apk”,optimizedDirectory 为使用范例中的目录的话,那么最终优化后的输出路径为/data/user/0/com.coder_yu.test/app_dex/plugin.dex,具体的目录在不同机型不同rom下有可能会不一样。
是时候看看DexFile了。在上面的loadDexFile方法中我们看到optimizedDirectory参数为null的时候直接返回new DexFile(file)了,否则返回 DexFile.loadDex(file.getPath(), optimizedPath, 0),但其实他们最终都是使用了相同方法去加载dexpath文件,因为DexFile.loadDex方法内部也是直接调用的了DexFile的构造器,以下:
然后看看DexFile的构造器吧
可以看到直接new DexFile(file)和DexFile.loadDex(file.getPath(), optimizedPath, 0)最终都是调用了openDexFile(sourceName, outputName, flags)方法,只是直接new的方式optimizedPath参数为null,这样openDexFile方法会默认使用 /data/dalvik-cache目录作为优化后的输出目录,第二个构造器的注释中写的很明白了。mCookie是一个int值,保存了openDexFile方法的返回值,openDexFile方法是一个native方法,我们就不深入了。
几种类加载器:
热修复核心逻辑:在DexPathList.findClass()过程,一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。也就是说当两个类不同的dex中出现,会优先处理排在前面的dex文件,这便是热修复的核心精髓,将需要修复的类所打包的dex文件插入到dexElements前面。
类加载过程常见的ClassNotFound原因:
好啦,文章写到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。