第四章 android热更新系列文章 之 ClassLoader
目录
热更新中最关键的部分就是classLoader
java中的classLoader种类和作用
- BootClassLoader
- PathClassLoader
- DexClassLoader
- BaseDexClassLoader
名称 | 作用 | 备注 |
---|---|---|
BootClassLoader | 类似于java中的BootStrap ClassLoader,加载androidFrameWork层的字节码文件 | |
PathClassLoader | 类似于java中的APP ClassLoader,加载已经安装到系统中的APP的字节码文件 | |
DexClassLoader | 类似于java中的Custom ClassLoader,用来加载指定目录下的字节码文件 | |
BaseDexClassLoader | 是BootClassLoader、PathClassLoader、DexClassLoader的父类 |
4.1 双亲代理模型的特点
classLoader在加载一个字节码文件的时候,首先会判断该类有没有被当前classLoader加载过,如果有直接使用,如果没有找到自己的父类(classLoader)判断有没有加载过,如果没有,就去找找父类的父类有没有加载过该文件,知道所有classLoader均为加载过该文件才会进行加载。
- 带来两个作用:
- a 类加载的共享功能
一些framework层架的类一但被我们的顶层的classLoader加载过,就会被缓存在内存里面,以后任何地方使用都不需要重新加载
- b 类加载的隔离功能
保证不同继承路线classLoadder加载的类不是同一个类,避免用户冒充系统写一个类,冒充系统的类。
怎样的类被认为是同一个类
必须是包名相同、类名相同、是被同一个classLoader加载的三个条件缺一不可
- 双亲代理模型的特点
一段APP代码的的运行至少需要多少个classLoader
需要两个,分别是BootClassLoader和PathClassLoader,一个加载framework层字节码,一个加载来自APP中的字节码文件
测试代码:
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
LogUtil.e("currentClassLoader'Name:____" + classLoader.toString());
while (classLoader.getParent() != null) {
LogUtil.e(classLoader.toString() + "parrentClassLoader'Name:____" + classLoader.getParent());
classLoader = classLoader.getParent();
}
}
文件输出:
dalvik.system.PathClassLoader[...]
parrentClassLoader'Name:____java.lang.BootClassLoader@23c1dc5
【注意】本文所参考的源码基于Android 7.1.2_r36 及以前版本,在Android 8.0.0_r4 之后,BaseDexClassLoader 、DexClassLoader 源码有所变动
-
PathClassLoader和DexClassLoader的区别
-
1、DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
-
2、PathClassLoader只能加载系统中已经安装过的apk
PathClassLoader 源码
以下源码全部来自Android6.0.1
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
/** 有兴趣的可以看看注释,故意没删
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
*
Raw ".dex" files (not inside a zip file).
*
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
DexClassLoader 源码
package dalvik.system;
import java.io.File;
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
* * <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
* * <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
原因
DexClassLoader构造函数
//dexPath :dex路径
//optimizedDirectory :指定输出dex优化后的odex文件,可以为null
//libraryPath:动态库路径(将被添加到app动态库搜索路径列表中)
//parent:制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
PathClassLoader构造函数
//dexPath :dex文件以及包含dex的apk文件或jar文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’
//optimizedDirectory :指定输出dex优化后的odex文件,可以为null
//libraryPath:动态库路径包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为null。(将被添加到app动态库搜索路径列表中)
//parent:制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
DexClassLoader 与 PathClassLoader 构造函数区别就是多了个optimizedDirectory参数,指定odex文件的输出路径。
DexClassLoader可以加载dex文件以及包含dex的apk文件或jar文件,也支持从SD卡进行加载,这也就意味着DexClassLoader可以在应用未安装的情况下加载dex相关文件。因此,它是热修复和插件化技术的基础
来查看它的代码,如下所示。
libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
DexClassLoader构造方法的参数要比PathClassLoader多一个optimizedDirectory参数,参数optimizedDirectory代表什么呢?我们知道应用程序第一次被加载的时候,为了提高以后的启动速度和执行效率,Android系统会对dex相关文件做一定程度的优化,并生成一个ODEX文件,此后再运行这个应用程序的时候,只要加载优化过的ODEX文件就行了,省去了每次都要优化的时间,而参数optimizedDirectory就是代表存储ODEX文件的路径,这个路径必须是一个内部存储路径。
PathClassLoader没有参数optimizedDirectory,这是因为PathClassLoader已经默认了参数optimizedDirectory的路径为:/data/dalvik-cache。DexClassLoader 也继承自BaseDexClassLoader ,方法实现也都在BaseDexClassLoader中。
BaseDexClassLoader 构造方法
//类路径: /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
最终我们得到如下的结论,当我们执行加载一个class文件时整个的执行流程是这样的:
-
加载一个class必须当前classLoader(PathClassLoader或者DexClassLoader)调用自己的loadClass(String className)方法
-
loadClass方法是接口classLoader定义的,真正的实现是在
BaseDexClassLoader
中的,BaseDexClassLoader
中loadClass()
方法做了如下事情,这里就是双亲委托模式最好的提现
2.1 首先调用自己的 findLoadedClass(String name)
方法,看自己有没有加载过这个class文件 ,没有执行2.2,如果加载过直接将加载过的字节码class文件返回,方法结束
2.2 查找自己父 classLoader
并调用其 findLoadedClass(String name)
方法,如果找到查看父类是否加载过这个class字节码文件,没有执行2.3,如果加载过直接将加载过的字节码class文件返回,方法结束
2.3 查看BootClassLoader
是否加载过这个class字节码文件,没有执行3,如果加载过直接将加载过的字节码class文件返回,方法结束
- 执行到这个过程,可以确认当前的class文件没有被加载过,当前classLoader调用 “BaseDexClassLoader” 的
findClass(String name)
方法查找对应class字节码文件,其中findClass(String name)
方法的实现如下
3.1 BaseDexClassLoader
的 findClass(name)
调用了DexPathList的 dexPathList.findClass(name,..)
方法,其中DexPathList 类的初始化是在BaseDexClassLoader的构造方法职工进行的,DexPathList是将de文件路径转化成文件的辅助类,可以将dex结尾的文件路径转换成文件,并生成Element数组
3.2 dexPathList
对象的创建是在BaseDexClassLoader
的构造方法中完成的,然后 dexPathList
调用了自己的 makeDexElements(splitDexPath(dexPath),optimizedDirectoryexcept,classLoader)
的将所有的dex路径转换成dexfile存到Element数组中,DexPathList类中 MakeDexElements()方法的内部实现如3.2.1
3.2.1 MakeDexElements()
方法的内部,将传入的文件路径分割遍历判断分割后的dex是一个文件还是文件夹;如果这个文件压缩文件进行解压缩,找到其中所有的dex文件存储到Elements 数组中,如果是文件夹,遍历获取其中所有的dex文件储到Elements 数组中,如果是dex文件,直接储到Elements 数组中,这一步调用完成,其实是获取到了一个载有dexFile文件的Elements数组
3.3 DexPathList
的内部实现,dexPathList的findClass(Element[] elements)方法的内部,遍历Element数组拿到里面的每一个Element对象,获取对象中成员变量DexFile,并调用DexFile的 loadClassBinaryName(name,classLoader,exception)
3.4 dexFile类中 loadClassBinaryName(name,classLoader,exception)
方法调用了自己的 defineClass(name,classLoader,cookie,DexFile,exception)
3.5 DexFile类中的 defineClass(name,classLoader,cookie,DexFile,exception)
方法最终调用了DexFile类中的 defineClassNative(name,classLoader,cookie,DexFile,exception)
本地方法
许多组件需要注册才能使用(像activity、service等组件)
资源动态加载复杂
- 资源在android中是用 id 索引的形式查找的,如果资源没有在清单文件中注册,报错资源找不到
总结:
android程序的运行需要一个上下文环境,这个正是第三方加载库需要解决的重点
文章参考:
Android解析ClassLoader(一)Java中的ClassLoader
热修复——深入浅出原理与实现
Android 插件化和热修复知识梳理