双亲委派机制
1. 什么是双亲委派机制
所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
虚拟机在加载类的过程中需要使用类加载器进行加载,**编写java程序过程中,有些是基础的系统类,有些是个人编写的类,如果个人的类与系统基础的类重复,就会破坏系统类整体正常使用 **,通过双亲委派机制,系统类使用系统的类加载器,个人编写的类使用应用或者个人级别的类加载器,从而系统类总是优先于个人类的加载,那么即使个人编写了与系统完全一致的类,加载过程中也会应为优先加载了系统类而导致个人的类不会加载
Java语言中支持的4种类加载器:
Java中提供的这四种类型的加载器,是有各自的职责的:
- Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等
- Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件
- Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
2.双亲委派是如何实现的
这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
ClassLoader.java
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}
Classloader间的组合关系如下
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 先让父加载器去加载,由于BootstrapClassLoader是根加载器,所以
//parent = null
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
}
//父classloader没有加载成功,那么使用自身的findclass(name)方法
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
Tips
责任链模式
defineClass()
将字节码转换为class对象
3. 认识Android中的classload
3.1 常见classloader
android对jvm做了调整,使用dvm虚拟机,类文件将被打包成dex
文件。底层的虚拟机是不同的,所以它们的类加载器当然也会不同
常见的Android类加载器有如下四种:
-
BootClassLoader :加载Android Framework层中的class字节码文件(类似java的BootstrapClassLoader)
-
PathClassLoader :加载已经安装到系统中的Apk的 class 字节码文件(类似java的 AppClassLoader )
-
DexClassLoader :加载制定目录的class字节码文件(类似java中的 Custom ClassLoader )
-
BaseDexClassLoader : PathClassLoader 和 DexClassLoader 的父类
3.2dvm中 classloader组合关系如下:
3.3 常见classloader源码
DexClassLoader.java
package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
//加载
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
}
android中类的加载流程由DexPathList.java
完成,DexPathList
的findClass(name)
//DexPathList的findClass,从自身的Element属性中查找,从前到后
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
3.4 热修复做法
**应用通过PathClassLoader
来加载用户编写的class,PathClassLoader
加载class的由DexPathList
完成 **,热修复便是通过反射修改DexPathList
的 dexElements
属性,将修复的文件插入dexElements
集合最前面,使得findClass
遍历时优先查找到修改后的class
要点:
- 通过反射可以获取app的
PathClassLoader
实例,pathList
属性实例和dexElements
属性实例 DexPathList
中包含方法将apk文件中的dex文件 提取出来,转化为Element
实例:
private fun installPatch(application: Application, patch: File) {
//原始dex文件
val classLoader = application.classLoader
Log.e("TAG", "classloader = ${classLoader::class.java.name}")
val pathListField = getField(classLoader::class.java, "pathList") ?: return
val pathList = pathListField.get(classLoader)
val dexElementsFiled = getField(pathList::class.java, "dexElements") ?: return
val oldElementArray = dexElementsFiled.get(pathList) as Array<*>
Log.e(
"TAG",
"dexElements = ${oldElementArray::class.java.name} ,size = ${oldElementArray.size}"
)
val patchList = ArrayList<File>().apply {
add(patch)
}
val patchElement: Array<*>? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
val makePathElement = getMethod(
pathList::class.java,
"makePathElements",
List::class.java,
File::class.java,
List::class.java
) ?: return
Log.e("TAG", "makePathElement != null")
val ioExceptions: ArrayList<IOException> = ArrayList()
makePathElement.invoke(
pathList,
patchList,
application.cacheDir,
ioExceptions
) as Array<*>
}
else -> {
val makePathElement = getMethod(
pathList::class.java,
"makeDexElements",
ArrayList::class.java,
File::class.java,
ArrayList::class.java
) ?: return
Log.e("TAG", "makeDexElements != null")
val ioExceptions: ArrayList<IOException> = ArrayList()
makePathElement.invoke(
pathList,
patchList,
application.cacheDir,
ioExceptions
) as Array<*>
}
}
val gapSize = patchElement?.size ?: 0
Log.e("TAG", "patchElement size = $gapSize , oldElementPathSize = ${oldElementArray.size}")
val fixArray = oldElementArray::class.java.componentType?.let {
java.lang.reflect.Array.newInstance(
it,
oldElementArray.size + gapSize
)
} as Array<Any?>
fixArray.forEachIndexed { index: Int, _: Any? ->
Log.e("TAG", "index = $index")
if (index + 1 <= gapSize) {
var patchElement1 = patchElement?.get(index)
Log.e("TAG", "ELEMENT TYPE = ${patchElement1.toString()}")
fixArray[index] = patchElement1
} else {
fixArray[index] = oldElementArray[index - gapSize]
}
}
dexElementsFiled.set(pathList, fixArray)
}
private fun getField(clazz: Class<out Any>, filedName: String): Field? {
var startClazz: Class<*>? = clazz
var filed: Field? = try {
startClazz?.getDeclaredField(filedName)
} catch (e: Exception) {
null
}
while (filed == null && startClazz != null) {
startClazz = startClazz.superclass
filed = try {
startClazz?.getDeclaredField(filedName)
} catch (e: Exception) {
null
}
}
return filed?.apply { isAccessible = true }
}
private fun getMethod(
clazz: Class<out Any>,
methodName: String,
vararg argusClazz: Class<out Any>
): Method? {
var startClazz: Class<*>? = clazz
var method: Method? = try {
startClazz?.getDeclaredMethod(methodName, *argusClazz)
} catch (e: Exception) {
null
}
while (method == null && startClazz != null) {
startClazz = startClazz.superclass
method = try {
startClazz?.getDeclaredMethod(methodName, *argusClazz)
} catch (e: Exception) {
null
}
}
return method?.apply { isAccessible = true }
}
使用:
private val fixPath = "app-debug.apk"
val path = Environment.getExternalStorageDirectory().absolutePath + File.separator + fixPath
val file = File(path)
if (!file.exists()) {
Log.e("TAG", "patch file is not exist , path = ${file.absoluteFile}")
} else {
Log.e("TAG", "patch file exist , path = ${file.absoluteFile}")
installPatch(this, file)
try {
} catch (e: Exception) {
Log.e("TAG", "message = ${e.message}")
}
}
Tips
- 补丁包可以打包成apk,或者是多个dex文件的zip
- fix文件需要与原文件保持相同包名和类名