1、简述
为了能够对热修复的原理理解的更加深入有必要对Android中的类加载机制进行解析。在分析之前首先来看看构造一个DexClassLoader并将dex文件构造成Element对象的时序图。
2、源码
在Andorid中有两个专门的类加载器用于加载Andorid的dex文件中的class文件,分别是PathClassLoader和DexClassLoader;PathClassLoader只能加载已经安装到Andorid系统中的apk文件(data/app目录),是Android系统默认的类加载器,DexClassLoader可以加载任意目录下的dex、jar、zip、apk文件,比PathClassLoader更加灵活,因此这也成为了实现热修复的一个突破点。下面对他们的代码分别进行讲解。
2.1 PathClassLoader源码讲解
该类继承了BaseDexClassLoader类,并且在仅有的两个构造方法中也调用到了父类的构造方法中。
public class PathClassLoader extends BaseDexClassLoader {
/**
* dexPath:要加载的dex、jar、apk或者zip文件string路径列表,并且每一个dex路径用:分隔开
* parent:父类加载
**/
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
/**
* librarySearchPath:加载程序文件时需要用到的库路径,有可能为null
**/
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
2.2 DexClassLoader源码讲解
该类也是继承了BaseDexClassLoader了,并且在仅有的一个构造方法中调用到了父类的构造方法。
public class DexClassLoader extends BaseDexClassLoader {
/**
* optimizedDirectory:dex文件的输出目录,因为在加载zip、apk、jar格式的程序文件的时候会解压出其中的dex文件,该目录
*就是专门用于存放这些被解压出来的dex文件,但是从api26开始就失效了,即使传入了具体的值也不会被使用。
**/
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
2.3 BaseDexClassLoader源码讲解
DexClassLoader和PathClassLoader类加载器最终都会调用到BaseDexClassLoader类中,也就是具体的实现都在该类中。
public BaseDexClassLoader (String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
从源码可以看出BaseDexClassLoader类继承了ClassLoader类,并且在BaseDexClassLoader的构造方法中首先会调用父类的构造方法,下面对ClassLoader中的构造方法进行分析。
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
在ClassLoader构造方法中会对父加载器进行初始化。接下来继续看BaseDexClassLoader的构造方法,初始化了成员变量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);
//对库文件路径进行解析
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
2.4 splitDexPath函数讲解
/**
* 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) {
try {
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode)) {
continue;
}
} catch (ErrnoException ignored) {
continue;
}
}
//将string列表中单个dex、jar、apk或者zip文件路径存放到list中
result.add(new File(path));
}
}
return result;
}
2.5 makeDexElements函数讲解
/**
* 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对象。
3、ClassLoader讲解
通过上述的过程将各个dex文件包装成了Element对象,但是也仅仅只是包装成了element对象而已,那实际的类加载是在什么地方加载的呢,看源码发现是在ClassLoader中的loadClass()方法中进行加载的。从上面的讲解中我们知道了PathClassLoader和DexClassLoader以及BaseDexClassLoader,除此之外还有BootClassLoader用于加载Android Framework层的class文件,接下来对BootClassLoader的初始化以及classLoader的源码进行讲解。
3.1 BootClassLoader初始化
阅读源码会发现BootClassLoader的初始化是在ZygoteInit类中初始化的,时序图如下所示:
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;
}
3.2 PathClassLoader的初始化
文章开头就已经知道了PathClassLoader是Andorid应用默认的类加载器,只能用于加载已经安装到系统中的apk中的class文件,那么为什么它会是默认的类加载器类,请看下面的分析。在ClassLoader中有如下的方法,看名字都知道是获取系统的类加载器
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
继续看SystemClassLoader中的load变量,发现是通过调用ClassLoader中的createSystemClassLoader()方法进行初始化的。
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());
}
而说它是默认的类加载器,是因为在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);
.......
}
看到这里我们终于知道了为什么PathClassLoader是Android应用默认的类加载器了并且只能加载已经安装到系统中的apk的class文件了。并且还知道了它的父类加载器是BootClassLoader。看源码发现在ZygoteInit函数中也调用了初始化了PathClassLoader,时序图如下所示:
3.3 loadClass方法
所有的class都会被ClassLoader中的loaderClass()函数加载,下面对该函数进行讲解。
/**
* name:class的名字
**/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
//从当前类加载器中查找是否已经加载过该类
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//如果没有当前的类加载器没有加载过该类并且父类加载器不为null,则到父类加载器中查找该类是否已经被加载过
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//从根加载器中查找该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//如果当前加载器没有加载并且父类加载器也没有加载那么则开始加载该类
c = findClass(name);
}
}
return c;
}
3.4 findClass方法
查看ClassLoader的findClass源码发现并没有任何实现,说明该方法是在子类中实现的,继续看DexClassLoader中的findClass方法。源码如下:
/**
* 遍历dexElements列表,找到与传入的className相对应的第一个class并返回;正因为这个特性成为了热修复的突破点,我们只需要
* 将需要修复的bug类编译成dex文件然后放到dexElements列表的第一个元素位置,当系统在查找类的时候就会只加载我们插入的dex
* 文件
* 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;
}
在BaseClassLoader的findClass方法中最终会调用到Element的findClass方法。
//最终会从dex文件查找该class,如果找到了则直接返回,没有找到则直接返回null
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
最终会调用到DexFile中的loadClassBinaryName方法
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;
}
5、双亲委托模型
在分析完了Android中类加载的大致过程之后,发现类加载的过程使用到了双亲委托模型,也就是某一个特定的类加载器在接到了加载类的请求的时候,会先将该请求委托给父类进行加载,如果父类加载成功了则直接返回,如果没有加载成功则由自己来进行加载。而所有的类加载器则形成了一个链状结构。
ClassLoader中的双亲委托模式:ClassLoader按级别分为了三个级别:
(1)最上级bootStrap ClassLoader(根类加载器):负责加载虚拟机的核心类库,如java.lang.*等,根类类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层的操作系统,属于虚拟机实现的一部分;
(2)中间级别extension ClassLoader(扩展类加载器):父类加载器是根加载器,它从java.ext.dirs系统属性所指定的目录中加载类库,用于加载Andorid Framework中的class文件。该加载器是纯java实现,也就是BootClassLoader类加载器;
(3)最低级别app ClassLoader(应用类加载器):它的父加载器是extension ClassLoader,它从环境变量或者系统属性java.class.path所指定的目录中加载类。也就是Andorid中的PathClassLoader类加载器。
6、热修复实现步骤
文章最开始就讲到Android中存在两个类加载器PathClassLoader和DexClassLoader,它们虽然都是为了将一个个dex文件构造成Element对象,并从dex文件中加载出对应的class文件,但是它们的使用方式却不相同。PathClassLoader是Android默认的dex文件加载器,DexClassLoader则是为了能够加载没有被初始化在apk中的代码,它可以加载Android中任意目录下包含dex的jar、apk、zip等文件,而这也成为了我们实现热修复的突破点。根据这种思路实现热修复大致步骤如下:
(1)将需要加入到原有apk的java文件编译为dex文件格式;
(2)获取到默认的PathClassLoader实例对象;
(3)获取指定目录下面所有包含dex文件的apk、jar、zip等文件;
(4)根据获取到的文件构造出DexClassLoader;
(5)获取到DexClassLoader中的dexElements列表,并存储到集合中;
(6)获取PathClassLoader中的dexElements列表;
(7)将获取到的dexElements列表集合按先后顺序存储到PathClassLoader中dexElements列表中的头部;
当app重新启动之后就会加载最新的dex文件,这样就会将Bug修复了,不过老的dex文件依旧存在于dexElements列表中,只是没有机会被加载到了而已。具体可以参考文章开头中这位大神的实现。
参看: