文章目录
ClassLoader简介
程序运行在虚拟机上时,虚拟机通过ClassLoader
把需要的Class
加载进来才能创建实例对象并工作。
Android
的Dalvik/ART虚拟机
如同标准的JVM虚拟机
一样,在运行程序时首先需要将对应的类加载到内存中。因此,我们可以利用这一点,在程序运行时手动加载Class
,从而达到代码动态加载可执行文件的目的。
Android
的Dalvik/ART虚拟机
虽然与标准的JVM虚拟机
不一样,ClassLoader
具体的加载细节不一样,但是工作机制是类似的。
ClassLoader 源码
在Android
中,ClassLoader
是一个抽象类,实际开发过程中,我们一般是使用其具体的子类DexClassLoader
、PathClassLoader
这些类加载器来加载类的,它们的不同之处是:
DexClassLoader
可以加载任意目录下的dex、jar、zip、apk文件,可以从SD卡中加载未安装的apk,比PathClassLoader
更加灵活。PathClassLoader
只能加载系统中已经安装过的apk(data/aap目录),是Android系统默认的类加载器。
PathClassLoader源码
点击查看AOSP源码
该类继承了BaseDexClassLoader
类,并且在仅有的两个构造方法中也调用到了父类的构造方法中。
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
public PathClassLoader(
String dexPath, String librarySearchPath, ClassLoader parent,
ClassLoader[] sharedLibraryLoaders) {
super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
}
DexClassLoader源码
点击查看AOSP源码
该类也是继承了BaseDexClassLoader
了,并且在仅有的一个构造方法中调用到了父类的构造方法。
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
optimizedDirectory
:dex文件的输出目录,因为在加载zip、apk、jar格式的程序文件的时候会解压出其中的dex文件,该目录
就是专门用于存放这些被解压出来的dex文件,但是从api26
开始就失效了,即使传入了具体的值也不会被使用
BaseDexClassLoader源码
public BaseDexClassLoader(String dexPath,String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
boolean isTrusted) {
super(parent); // 对父加载器进行初始化
...
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); // 初始化成员变量pathList
...
}
–>继续看DexPathList
中的构造方法
/**
* definingContext:当前的类加载器
* dexPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
* librarySearchPath:加载程序文件的库文件
* optimizedDirectory:dex文件的解压目录,但是在api26以后就不在使用了
**/
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
..........//判断数据的合法性
this.definingContext = definingContext;
//初始化IO异常列表
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//将dex文件构造为Elements对象
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
.....
}
–> splitDexPath
,它分隔string路径dex列表到一个list中,以:分隔
/**
* searchPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
**/
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
List<File> result = new ArrayList<>();
if (searchPath != null) {
for (String path : searchPath.split(File.pathSeparator)) {
if (directoriesOnly) {
StructStat sb = Libcore.os.stat(path);
....
}
//将string列表中单个dex、jar、apk或者zip文件路径存放到list中
result.add(new File(path));
}
}
return result;
}
–>makeDexElements
,它把分隔的dex路径列表解析成Element
列表
/**
* files:dex、jar、zip或者apk文件路径列表
* optimizedDirectory: dex解压路径,在api26以后为null
* suppressedExceptions:IO异常列表
**/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
//文件是一个目录,则直接添加到elements列表中,后续解析的时候直接从目录中找到dex文件
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
//如果该文件是.dex结尾的文件则将该文件包装为DexFile对象
if (name.endsWith(DEX_SUFFIX)) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
//如果该文件是jar、apk或者zip文件,则从这些文件中提取出dex文件并包装成DexFile对象,具体的提取是在DexFile
//中通过native方法进行提取
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
//如果实际的长度和理论的长度不等,则将elements的长度变更为实际长度
//实际长度<=理论长度
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
从PathClassLoader
和DexClassLoader
的构造方法开始,最后会在BaseClassLoader
中将包含dex的文件
或者文件夹
构造成一个个的Element
对象。
ClassLoader讲解
通过上述的过程将各个dex文件
包装成了Element
对象,那实际的类加载是在什么地方加载的呢,看源码发现是在ClassLoader
中的loadClass
方法中进行加载的。
从上面的讲解中我们知道了PathClassLoader
和DexClassLoader
以及BaseDexClassLoader
,除此之外还有BootClassLoader
用于加载Android Framework层的class文件,接下来对BootClassLoader
的初始化以及loadClass
的源码进行讲解。
BootClassLoader初始化
BootClassLoader
的初始化是在ZygoteInit类中初始化的, 简化代码如下:
public static void main(String argv[]) {
ZygoteServer zygoteServer = new ZygoteServer();
....
preload(bootTimingsTraceLog);
...
}
-->
static void preload(TimingsTraceLog bootTimingsTraceLog) {
...
preloadClasses();
...
}
-->
private static void preloadClasses() {
...
Class.forName(line, true, null);
}
最后会调用到Class的forName
函数里面:
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)throws ClassNotFoundException {
//如果classLoader为null那么创建BootClassLoader
if (loader == null) {
loader = BootClassLoader.getInstance();
}
Class<?> result;
try {
//通过class的包名从classLoader中查找我们需要的class文件并返回
result = classForName(name, initialize, loader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof LinkageError) {
throw (LinkageError) cause;
}
throw e;
}
return result;
}
看到这里发现这不是我们在使用反射的时候获取class的方法吗: result = classForName(name, initialize, loader);
PathClassLoader的初始化
PathClassLoader
是Andorid应用默认的类加载器,只能用于加载已经安装到系统中的apk中的class文件,那么为什么它会是默认的类加载器类,请看下面的分析。
在ClassLoader
的默认构造函数中,有如下方法:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
getSystemClassLoader
看名字都知道是获取系统的类加载器。
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
-->loadr来源
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
-->createSystemClassLoader来源
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
这里可以看出SystemClassLoader
创建的类加载器是PathClassLoader
对象,并且它的父类加载器是BootClassLoader
。
说它是默认的类加载器,是因为在ActivityThread
的performLaunchActivity
方法中,通过SystemClassLoader
方法或者直接生成PathClassLoader
对象的方式获取到了PathClassLoader
,并且用于构造新的Activity
,部分源码如下所示:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
........
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
........
}
-->调用到ContentImpl类中的getClassLoader,如果mPackageInfo为null则通过ClassLoader中的SystemClassLoader直接返回PathClassLoader
public ClassLoader getClassLoader() {
return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
-->调用到LoadedAPK中的getClassLoader函数
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
// 如果是系统
if (mPackageName.equals("android")) {
if (mClassLoader != null) {
return;
}
if (mBaseClassLoader != null) {
mClassLoader = mBaseClassLoader;
} else {
mClassLoader = ClassLoader.getSystemClassLoader();
}
return;
}
........
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
libraryPermittedPath, mBaseClassLoader);
........
}
-->最后会到ApplicationLoaders中通过工厂模式生成PathClassLoader并返回
private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
String librarySearchPath, String libraryPermittedPath,
ClassLoader parent, String cacheKey) {
.......
PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
zip,
librarySearchPath,
libraryPermittedPath,
parent,
targetSdkVersion,
isBundled);
.......
}
loadClass方法
所有的class都会被ClassLoader
中的loaderClass
函数加载,下面对该函数进行讲解。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
-->
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 1. 从已加载的类的缓存中查找。
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 2.如果父加载器不为空,则委托父加载器加载。
c = parent.loadClass(name, false);
} else {
// 3.如果父加载器为空,则委托 bootstrap 加载器加载。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//4.最后调用 `findClass(name)` 加载。
c = findClass(name);
}
}
return c;
}
从源码中我们也可以看出,loadClass
方法在加载一个类的实例的时候
findLoadedClass(name)
从已加载的类的缓存中查找。parent.loadClass(name, false)
如果父加载器不为空,则委托父加载器加载,可以看出这其实是一个向上递归的过程,findBootstrapClassOrNull(name)
如果父加载器为空,则委托 bootstrap 加载器加载。findClass(name)
最后调用findClass(name)
加载。
findClass方法
查看ClassLoader
的findClass
源码发现并没有任何实现,说明该方法是在子类中实现的,继续看DexClassLoader
中的findClass
方法, 也即是BaseDexClassLoader
中的findClass
方法,内部调到了DexPathList
的findClass
方法。源码如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
.......
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
.....
return c;
}
--->DexPathList的源码
/**
* name: 需要寻找的class名
* suppressed: 异常列表
**/
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
遍历dexElements
列表,找到与传入的className
相对应的第一个class并返回;正因为这个特性成为了热修复
的突破点,我们只需要 将需要修复的bug类编译成dex文件然后放到dexElements列表的第一个元素位置,当系统在查找类的时候就会只加载我们插入的dex 文件。
最终会调用到Element
的findClass
方法, Element
是DexPathList
内部的一个静态类:
static class Element {
.....
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
}
最终会调用到DexFile
中的loadClassBinaryName
方法:
点击查看AOSP源码
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
//调用到native层从dex文件中查找到与name相对应的clas文件
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
双亲委托模型
在分析完了Android中类加载的大致过程之后,发现类加载的过程使用到了双亲委托模型,也就是某一个特定的类加载器在接到了加载类的请求的时候,会先将该请求委托给父类进行加载(递归向上查询),如果父类加载成功了则直接返回,如果没有加载成功则由自己来进行加载。而所有的类加载器则形成了一个链状结构。
ClassLoader
中的双亲委托模式:ClassLoader
按级别分为了三个级别:
1.最上级bootStrap ClassLoader
(根类加载器):负责加载虚拟机的核心类库,如java.lang.*
等,根类类加载器从系统属性sun.boot.class.path
所指定的目录中加载类库。根类加载器的实现依赖于底层的操作系统,属于虚拟机实现的一部分。
2.中间级别extension ClassLoader
(扩展类加载器):父类加载器是根加载器,它从java.ext.dirs
系统属性所指定的目录中加载类库,用于加载Andorid Framework
中的class
文件。该加载器是纯java实现,也就是BootClassLoader
类加载器。System
类的加载器就是BootClassLoader
。
3.最低级别app ClassLoader
(应用类加载器):它的父加载器是extension ClassLoader
,它从环境变量或者系统属性java.class.path
所指定的目录中加载类。也就是Andorid中的PathClassLoader
类加载器。
测试代码:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testClassLoaderNum();
}
void testClassLoaderNum() {
int i = 0;
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e("hgy413", "[ClassLoader]:" + i + ":" + classLoader.toString());
i++;
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.e("hgy413", "[ClassLoader]:" + i + ":" + classLoader.toString());
i++;
}
}
}
输出结果为
hgy413: [ClassLoader]:0:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hgy413.classloader-1/base.apk"],nativeLibraryDirectories=[/data/app/com.hgy413.classloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
/hgy413: [ClassLoader]:1:java.lang.BootClassLoader@e6bc431
可以看到有2个Classloader
实例,一个是BootClassLoader
(系统启动的时候创建的),另一个是PathClassLoader
(应用启动时创建的,用于加载"/data/app/com.hgy413.classloader-1/base.apk”
里面的类)。由此也可以看出,一个运行的Android应用至少有2个ClassLoader。
双亲委托模型的好处
按上面的机制,可以看出作用是防止内存中出现多份同样的字节码
比如两个类A和类B都要加载System
类
类A会递归的向父类查找,也就是首选用BootClassLoader
尝试加载,如果找不到再向下。这里的System
就能在BootClassLoader
中找到然后加载。
如果此时类B也要加载System
,也从BootClassLoader
开始,此时BootClassLoader
发现已经加载过了System
,那么直接返回内存中的System
即可, 而不需要重新加载,这样内存中就只有一份System
的字节码了。
同时也避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。这也好理解,一些系统层级的类会在系统初始化的时候被加载,比如java.lang.String
,如果在一个应用里面能够简单地用自定义的String
类把这个系统的String
类给替换掉,那将会有严重的安全问题。
热修复实现
同一种类型的类定义
如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader
会一直优先使用旧类。
如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader
没有树的继承关系的另一个ClassLoader
来加载新类,因为ClassLoader
只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader
没有继承关系,那么旧类和新类都能被加载。
不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。
同一个Class = 相同的 ClassName + PackageName + ClassLoader
以上问题在采用动态加载功能的开发中容易出现,请注意。
热修复实现步骤
文章最开始就讲到Android中存在两个类加载器PathClassLoader
和DexClassLoader
,它们虽然都是为了将一个个dex文件
构造成Element
对象,并从dex文件
中加载出对应的class文件,但是它们的使用方式却不相同。
PathClassLoader
是Android默认的dex文件加载器,DexClassLoader
则是为了能够加载没有被初始化在apk中的代码,它可以加载Android中任意目录下包含dex的jar、apk、zip等文件,而这也成为了我们实现热修复的突破点。根据这种思路实现热修复大致步骤如下:
- 将需要加入到apk的
java文件
编译为dex文件
格式 - 获取到默认的
PathClassLoader
实例对象 - 获取指定目录下面所有包含
dex文件
的apk、jar、zip等文件 - 根据获取到的文件构造出
DexClassLoader
- 获取到
DexClassLoader
中的dexElements
列表,并存储到集合中 - 获取
PathClassLoader
中的dexElements
列表 - 将获取到的
dexElements
列表集合按先后顺序存储到PathClassLoader
中dexElements
列表中的头部
当app重新启动之后就会加载最新的dex文件,这样就会将Bug修复了,不过老的dex文件依旧存在于dexElements
列表中,只是没有机会被加载到了而已。
热修复详细步骤
参考:
Android动态加载基础 ClassLoader工作机制
Android中类加载机制
热修复——深入浅出原理与实现