前言:
接着上一次02-为什么dex文件比class文件更适合移动端?的继续往下,距离上一篇已经过去快半年了,从我的博文记录中就可以清楚地看到:
转眼2023年新春假期接近尾声了,在这近半年的时间里,其实发生了很多事,有伤心、有焦虑,当然也有开心和希望,其中还经历过人生中最最艰难的时期,还好在朋友的帮助下给抗过来了【关于这些故事这里就不分享了,默默藏在心里就好】。当然最最让我伤心的是,坚持了这么久的博客彻底地被放弃了,当你没有了节奏感之后,再拣回来是需要付出很大的代价的,这不新年来了嘛,必须要有个新气象,新气象打算先从恢复博客开始,这里接着Android面试题的系列章节继续往下,好,费话不多说,直接开干。
题面解析:
关于这道题,从题面来看,很显然是考ClassLoader相关的东东,而一谈到ClassLoader,就会想到它的双亲委派模型,关于这块的基础可以参考我之前所记录的类加载器双亲委托机制详解,说实话这块基本学了就忘,所以正好可以借此机会再来复习一下。其中这里有两个加粗的词:“ClassLoader”、“双亲委派模型”,是的,要想答好此题,需要从这俩角度来进行剖析,那都有哪些点呢,下面列一下:
ClassLoader:
对于它,主要需要掌握如下知识点:
1、它是做什么的?
2、它的加载过程是怎么样的?
3、它把class加载给谁?
双亲委派模型:
对于它,则需要从双亲委派模型的“源码”出发,然后再来掌握它的原理。
总结:
当以上两个知识点都掌握之后,接下来还有一些可以说的:
1、说一下ClassLoader和双亲委派模型这样设计的一个好处?
2、双亲委派模型它是一种规则,而有些情况下是需要打破这种双亲委派模型的,那如何打破呢?
是不是这么一解析,要想答好这道题,其实不是那么简单的,而如果按照这么一个思路去解答,就会做到有理有据。
本题得分点:
接下来看一下得分点,还是如之前所学习的,从以下两个角度来看:
知识储备:
主要是ClassLoader、方法区、双亲委派模型。
技术思考:
1、双亲委派解决了什么问题?
2、如何打破双亲委派模型?
字节码加载:
先来回顾一下字节码class文件加载的过程, 如下图:
可以看到.class字节码文件是通过ClassLoader来加载到运行时数据区的“方法区”中的。
方法区:
那方法区主要是作用是啥呢?其实就是在内存中,存放class文件的逻辑结构,也就是类的元(meta)信息, 其中就包括在上一次02-为什么dex文件比class文件更适合移动端?所学习到的:常量池、类信息、字段、方法、属性等。
方法区实现:
我们知道JVM只是一个规范,而方法区其实也只是一个规范,它有不同的实现方式。
- 在Java8以前的版本,被实现“永久代”,名称与堆中的“年轻代”、“养老代”相对应,和堆一样,同为线程共有,但又没有垃圾回收【这个比较容易理解,因为方法区是存放的类的元(meta)信息,如果它能够被回收,是不是意味着我们就不能创建类的实例了?】,所以又被称为“非堆”。
- 在Java8以后的版本,被称为元空间(meta space),直接放在本地内存,所以理论上没有大小上限。
Java程序的双亲委派模型:
先来了解Java程序的双亲委派模型,说到“Java程序”,很明显对于Android来说还有它自身的双亲委派模型,这个之后就会谈到,这里先来看下Java程序的双亲委派,先上个图:
关于这块的介绍在之前类加载器双亲委托机制详解已经详细有说明,这里就当大概复习一下核心点,对于要注意:从图中貌似看这些加载类是一个父子的树形结构对吧,其实实际不是:
而标红的是三大系统加载类,最底层的是自己自定义的加载类,对于它们加载的职责也是不一样的,简单说明一下:
其中位于jdk中的rt.jar熟悉吧,它里面的类就是由Bootstrap ClassLoader来进行加载的,如我们本题所探讨的java.lang.Object这个类。
Android程序的双亲委派模型:
概述:
好,接下来则来看一下Android的双亲委派模型了,这块也是咱们最关注的,肯定是跟Java有些不同点的,先上张图:
对比Java的双亲委派模型,系统类加载器由三个变成了二个了,其中PathClassLoader类似于Java中的Application ClassLoader,而自定义ClassLoader变成了DexClassLoader了,很明显在Android中加载的是dex文件了,
类图:
在正式看这块源码之前,先来看一下整体的双亲委派的类图,如下:
ClassLoader源码:
findClass():
好,接下来则先从ClassLoader的源码开始分析,这里只分析主流程,一些不相关的直接省略掉有助于程序的理解,先从入口开始:
这个类应该也是普遍比较熟悉的,它的具体实现是由子类来决定的,这样就使得不同层级的ClassLoader就可以实现去不同的位置加载类的需求了,比如启动类加载器就可以加载jre-lib下的class,应用加载器就可以加载我们工程编译的class,
loadClass():
接下来另一个重要方法就是加载类的方法了,先来看一下它的实现:
这块实现也是大家比较熟悉的了,其中很明显是先读取缓存,如果该类已经被加载了则直接返回,如果木有被加载,则会递归调用父类加载器的loadClass:
而如果父类加载器都加载不到该类,则会交由当前的类加载器进行加载:
其双亲委派的机制就源自于这个类加载器的源代码逻辑。
加载自己写的Object类会发生什么?
好,在了解了双亲委派机制之后, 接下来我们就可以来分析一下,如果自己写了一个java.lang.Object的类,会发生啥?
1、自定义DexClassLoader会递归委派给父类加载器进行加载:
也就是:
接下来父类都未加载过,则会递归委派,如下:
2、启动类加载器会加载framework中的Object类:
根据源码,由于启动类加载器已经是顶层加载器,木有父类了,则此时就会交由自身进行加载,而启动类加载器就会加载framework中的Object类了:
3、总结:
通过上面的分析, 是不是发现,有了这个双亲委派模型,就始终都未执行过自定义DexClassLoader的findClass()了,加载的永远是android framework层的class了?所以很明显对于这题的答案就是“能自己写一个java.lang.Object的类,但是永远不会被加载进来,因为java.lang.Object是系统中的类,会被启动类加载。”
Class的双亲委派模型有什么好处?
接下来再来看一下对于有了双亲委派模型之后,它带来的好处:
1、能够对类划分优先级层次关系;
我们自己写的类,永远都是要比系统的类层次要低的。
2、避免类的重复加载;
一旦加载了某个类,相关的子类加载器就没有机会重复加载这个类了。
3、沙箱安全机制,避免代码被篡改:
系统的核心类库对于外界来说就是一个沙箱,我们无法通过正常的代码来干涉核心类库的执行,同样的自定义类加载器也无法干预我们写的应用程序的代码。
为什么要打破双亲委派模型呢?
对于双亲委派模型有这么多好处,那何时需要打破这种它,下面举几个例子:
1、解决某些版本冲突问题:
比如:
也就是B类加载器加载了某类库的1.0版本,但是呢对于它的子类加载器c必须要使用该库的2.0的版本,如果按照双亲委派的原则,很明显是办不到的,因为C加载器永远没有办法加载2.0的版本了,此时双亲委派模型就需要打破了,可以这么做:
也就是让子类C加载器越过加载器B,直接成为加载器A的子类加载器,此时就可以让C类加载器使用2.0的版本了,当然前提是子类加载器B和C木有过多的关联关系。
注意: 这只是一个理论上可行的方案【也就是在面试时让面试官了解自己是懂类加载器机制的】,实际开发中有更好的方式来解决版本冲突,比如使用gradle的脚本。
2、 热部署(重复加载已经被加载的类):
这个需求很明显就已经违背了双亲委派模型了【因为已经加载的类,是不允许再次加载的】,此时就需要打破双亲委派了,下面具体来看一下这种场景:
比如有一个热部署的类叫Operation,然后做法就是用一个子类加载器C来加载:
注意,此时C加载器不能成为B类加载器直接或间接的子类加载器,此时C加载器就可以重新加载Operation了,另外它只能在子ClassLoader C加载的类范围内生效:
只能说是一个有限的热部署,这里也是仅限面试时可以提一下,而对于Android来说,通常会想到热更新对吧,它那实现就比较复杂,比如Tinker,关于它的思想可以参考之前学习的这篇:手写热更新阐述tinker实现原理。
解题总结:
问:你能不能自己写一个叫做java.lang.Object的类?
答:能写,能通过编译,但是不会被加载,其不被加载的原因是由于有类加载的双亲委派模型所决定【这里就可以展开对双亲委派机制的描述了】,而如果想要被加载就需要打破双亲委派模型了【这里就又可以展开打破双亲委派机制的知识点进行阐述了】。
关注个人公众号,获得实时推送