实体类里的内部类怎么单独赋值_浅谈Android类加载器

一. 概述
Android从5.0开始就采用art虚拟机, 该虚拟机有些类似Java虚拟机, 程序运行过程也需要通过ClassLoader 将目标类加载到内存.
传统Jvm主要是通过读取class字节码来加载, 而art则是从dex字节码来读取. 这是一种更为优化的方案, 可以将多个.class文件合并成一个classes.dex文件. 下面直接来看看ClassLoader的关系。二. 五种类构造器
接下来依次看看PathClassLoader,DexClassLoader,BaseDexClassLoader,BootClassLoader,ClassLoader这5个类加载器。
PathClassLoader和DexClassLoader,它们都继承自BaseDexClassLoader,这两个类有什么区别呢?其实看一下它们的源码注释就一目了然了。因为代码很少,约等于没有,这里直接贴出它们的源码,其实主要是注释:2.1 PathClassLoader

6e7534d01ee89397e162a3607c759731.png

7089f93cbc3ba94b06b2b160462228a3.png

由注释看可以发现PathClassLoader被用来加载本地文件系统上的文件或目录,但不能从网络上加载,关键是它被用来加载系统类和我们的应用程序,这也是为什么它的两个构造函数中调用父类构造器的时候第二个参数传null,具体的参数意义请看接下来DexClassLoader的注释。

2.2 DexClassLoader

77eb5b1c373c2b35fe9383f0f1f3a60b.png

DexClassLoader用来加载jar、apk,其实还包括zip文件或者直接加载dex文件,它可以被用来执行未安装的代码或者未被应用加载过的代码。这里也写出了它需要的四个参数的意思

  1. dexPath:需要被加载的文件地址,可以多个,用File.pathSeparator分割
  2. optimizedDirectory:dex文件被加载后会被编译器优化,优化之后的dex存放路径,不可以为null。注意,注释中也提到需要一个应用私有的可写的一个路径,以防止应用被注入攻击,并且给出了例子 File dexOutputDir = context.getDir("dex", 0);
  3. libraryPath:包含libraries的目录列表,plugin中有so文件,需要将so拷贝到sd卡上,然后把so所在的目录当参数传入,同样用File.pathSeparator分割,如果没有则传null就行了,会自动加上系统so库的存放目录
  4. parent:父类构造器

这里着重看一下第二个参数,之前说过PathClassLoader中调用父类构造器的时候这个参数穿了null,因为加载app应用的时候我们的apk已经被安装到本地文件系统上了,其内部的dex已经被提取并且执行过优化了,优化之后放在系统目录/data/dalvik-cache下。

2.3 BaseDexClassLoader

0fd6cab894a8ddd82cb965d5a553a961.png

BaseDexClassLoader构造函数, 有一个非常重要的过程, 那就是初始化DexPathList对象.

另外该构造函数的参数说明:

  • dexPath: 包含目标类或资源的apk/jar列表;当有多个路径则采用:分割;
  • optimizedDirectory: 优化后dex文件存在的目录, 可以为null;
  • libraryPath: native库所在路径列表;当有多个路径则采用:分割;
  • ClassLoader:父类的类加载器.

2.4 BootClassLoader

2d74659d3fbce563971a95d1f6b3afcd.png

2.5 ClassLoader

71855cfc767257228fc1db99d32fbc50.png

再来看看SystemClassLoader,这里的getSystemClassLoader()返回的是PathClassLoader类。

3cc900be1dafdeca1abdca07ea3e3236.png

三. 类加载实例

首先看一段如何使用类加载器加载的调用代码:

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类的构造器

c41bdb6cbb0b580284b099cb47682f6a.png

首先也是调用了父类的构造器,但这里只将parent传给父类,即ClassLoader,ClassLoader中做的也很很简单,它内部有个parent属性,正好保存传进来的参数parent,这里可以稍微看一下第二个参数的注释,最后一句说到可以为null,而是否为null又刚好是PathClassLoader和DexClassLoader的区别,那是否为null最终又意味着什么呢?

94fb6f8729a4206b36de2732c494b615.png

接下来BaseDexClassLoader给originalPath 和 pathList赋了值,originalPath就是我们传进入的dex文件路径,pathList 是一个new 出来的DexPathList对象。

2810417b76169e5ad4c857544e67bb7f.png

别的先不说,先看注释。第四个参数中说到如果optimizedDirectory 为null则使用系统默认路径代替,这个默认路径也就是/data/dalvik-cache/目录,这个一般情况下是没有权限去访问的,所以这也就解释了为什么我们只能用DexClassLoader去加载类而不用PathClassLoader。

然后接着看代码,显然,前面三个if判断都是用来验证参数的合法性的,之后同样只是做了三个赋值操作,第一个就不说了,保存了实例化DexPathList的classloader,第二个参数的声明是一个Element数组,第三个参数是lib库的目录文件数组。

aafb89a83b9b2cb0997d5d59396635f7.png

看它们之前先看看几个split小函数:

11e5fd9c15a6d565402e1a158d3b8ac2.png

这两个顾名思义就是拿来分割dexPath和libPath,它们内部都调用了splitPaths方法,只是三个参数不一样,其中splitLibraryPath方法中调用splitPaths时的第二个参数仿佛又透露了什么信息,没错,之前介绍DexClassLoader参数中的libraryPath的时候说过,会加上系统so库的存放目录,就是在这个时候添加上去的。

ada10bd2b0cd612df5de82fc9ccc6354.png

什么啊,原来这个方法也没做什么事啊,只是把参数path1和参数path2又分别调用了下splitAndAdd方法,但是这里创建了一个ArrayList,而且调用splitAndAdd方法的时候都当参数传入了,并且最终返回了这个list,所以我们大胆猜测下,path1和path2最后被分割后的值都存放在了list中返回,看下是不是这么一回事吧:

fe13f64ac539bc112f20e74bc81cec8d.png

果然,跟我们猜的一样,只是又加上了文件是否存在以及是否可读的验证,然后根据参数wantDirectories判断是否文件类型是被需要的类型,最终加入list。现在我们回过头去看看splitDexPath方法和splitLibraryPath方法,是不是一目了然了。

再往上看DexPathList的构造器,nativeLibraryDirectories的最终值也已经知道了,就差dexElements了,makeDexElements方法的两个参数我们也已经知道了,那我们就看看makeDexElements都干了些什么吧。

949376e59b2840a5a860456c420b517e.png

60b651f3f9591a8e0b7f7b19f6791039.png

方法也不长,我们一段段看下去。首先创建了一个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了。

f7023946070f308e474d2ac116bc0f5d.png

很简洁,如果optimizedDirectory == null则直接new 一个DexFile,否则就使用DexFile.loadDex来创建一个DexFile实例。

0cc3f68a0065972fb7759795a142208b.png

这个方法获取被加载的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的构造器,以下:

11277d5e702220c507477719be43f27a.png

然后看看DexFile的构造器吧

aa3882cf1a985c16e8fff12d6eda5e75.png

可以看到直接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方法,我们就不深入了。

四. 总结

几种类加载器:

  • PathClassLoader: 主要用于系统和app的类加载器,其中optimizedDirectory为null, 采用默认目录/data/dalvik-cache/
  • DexClassLoader: 可以从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载,但必须是app私有可写目录来缓存odex文件. 能够加载系统没有安装的apk或者jar文件, 因此很多插件化方案都是采用DexClassLoader;
  • BaseDexClassLoader: 比较基础的类加载器, PathClassLoader和DexClassLoader都只是在构造函数上对其简单封装而已.
  • BootClassLoader: 作为父类的类构造器。

热修复核心逻辑:在DexPathList.findClass()过程,一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。也就是说当两个类不同的dex中出现,会优先处理排在前面的dex文件,这便是热修复的核心精髓,将需要修复的类所打包的dex文件插入到dexElements前面。

类加载过程常见的ClassNotFound原因:

  • ABI异常:常见在系统APP,为了减小system分区大小会将apk源文件中的classes.dex文件移除,对于既然可运行在64位又可运行在32位模式的应用,当被强制设置32位时,openDexFileNative在查找不到oat文件时会运行在解释模式,而classes.dex文件不再则出现ClassNotFound异常。
  • MultiDex处理不当,由于每个Dex文件中方法个数不能超过65536,引入MultiDex机制。dex2oat会自动查找Apk文件中的classes.dex,classes2.dex,…classesN.dex等文件,编译到/data/dalvik-cache下生成oat文件。这里需要文件名跟classesN.dex格式,并且一定要与classes.dex一起放置在第一级目录,有些APP不按照要求来,导致ClassNotFound异常。

好啦,文章写到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

1fb82a923845ac0efab61863c333f770.png

希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

转发+点赞+关注,第一时间获取最新知识点

Android架构师之路很漫长,一起共勉吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值