前言
一般说起 PathClassLoader 和 DexClassLoader ,大家都会说,前者只能加载内存中已经安装的apk中的dex,而后者可以加载sd卡中的apk/jar ,因此 DexClassLoader 是热修复和插件化的基础。但是具体为什么DexClassLoader能加载sd卡中的类,很多文章都只是一笔带过 ,于是研究了下源码,做个记录。
【注意】本文所参考的源码基于Android 7.1.2_r36 及以前版本,在Android 8.0.0_r4 之后,BaseDexClassLoader 、DexClassLoader 源码有所变动
PathClassLoader
//类路径:/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
DexClassLoader
//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
可以看到,两者都是继承自BaseDexClassLoader
,构造方法的具体逻辑在父类中实现,唯一不同的是一个参数:optimizedDirectory
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);
}
BaseDexClassLoader
的构造方法,只是用这些参数,创建了一个DexPathList
的实例,DexPathList 使用 Element 数组存储了所有的 dex 信息,在 dex 被存到 Element 数组后,所有的类都会在 Element 数组中寻找,不会再次从文件中加载
//类路径:/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//从pathList的Element数组中找类,找不到就报ClassNotFoundException
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
DexPathList
DexPathList 通过 makeDexElements
得到了一个 Element[]
类型的 dexElements
对象数组,里面存放了app的所有dex相关信息。这里仍然看不出 PathClassLoader 和 DexClassLoader 的具体差别,只知道前者参数中的 optimizedDirectory
传的是 null 。
//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//...略
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions,definingContext);
//...略
}
在 makeElements
中,使用for,循环调用了 loadDexFile 来加载 dexPath 中每个目录中的 dex
dex = loadDexFile(file, optimizedDirectory, loader, elements);
再看 loadDexFile 函数:
//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
根据 optimizedDirectory
参数是否为空,执行的方法不同,PathClassLoader
执行的是 new DexFile()
, 而 DexClassLoader
执行的是 DexFile.loadDex()
, 然而 loadDex 最终也还是会调用 new DexFile()
来创建实例。
DexFile
//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
static DexFile loadDex(String sourcePathName, String outputPathName,
int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
}
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
mFileName = sourceName;
}
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
private static native Object openDexFileNative(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements);
在 DexFile 的构造方法中,调用了 openDexFile
去生成一个 mCookie
,可以看到,不管是哪个类型的ClassLoader,最终都会调用 native 方法 openDexFileNative
来实现具体的加载逻辑。
Native 层的源码追踪
DexFile_openDexFileNative
static jint DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
//...略
const DexFile* dex_file;
if (outputName.c_str() == NULL) {// 如果outputName为空,则dex_file由sourceName确定
dex_file = linker->FindDexFileInOatFileFromDexLocation(dex_location, dex_location_checksum);
} else {// 如果outputName不为空,则在outputName目录中去寻找dex_file
std::string oat_location(outputName.c_str());
dex_file = linker->FindOrCreateOatFileForDexLocation(dex_location, dex_location_checksum, oat_location);
}
//...略
return static_cast<jint>(reinterpret_cast<uintptr_t>(dex_file));
}
判断传入的 outputName
是否为空,分别执行不同的方法,这个 outputName 就是 BaseDexClassLoader 构造方法中传入的 optimizedDirectory
参数,辗转来到这里。
- 当 outputName 不为空时【DexClassLoader】
执行FindOrCreateOatFileForDexLocation
函数,通过outputName
拿到oat_location
,然后尝试调用FindDexFileInOatLocation
从 oat_location 中寻找到 dex ,这就是我们经常用到到热修复的原理了,通过在sd卡中存放新的补丁dex/jar/apk替代旧的,来实现更新。
const DexFile* ClassLinker::FindOrCreateOatFileForDexLocation(const std::string& dex_location,uint32_t dex_location_checksum,const std::string& oat_location) {
WriterMutexLock mu(Thread::Current(), dex_lock_); // 互锁
return FindOrCreateOatFileForDexLocationLocked(dex_location, dex_location_checksum, oat_location);
}
const DexFile* ClassLinker::FindOrCreateOatFileForDexLocationLocked(const std::string& dex_location,uint32_t dex_location_checksum,const std::string& oat_location) {
const DexFile* dex_file = FindDexFileInOatLocation(dex_location,dex_location_checksum,oat_location);
if (dex_file != NULL) {
// 如果顺利打开,则返回
return dex_file;
}
const OatFile* oat_file = OatFile::Open(oat_location, oat_location, NULL,!Runtime::Current()->IsCompiler());
if (oat_file == NULL) {
return NULL;
}
const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(dex_location, &dex_location_checksum);
if (oat_dex_file == NULL) {
return NULL;
}
const DexFile* result = oat_dex_file->OpenDexFile();
return result;
}
- 当 outputName 为空时【PathClassLoader】
执行FindDexFileInOatFileFromDexLocation
函数,从 dex_location 中拿到 dex 文件,这个 dex_location 也就是 BaseDexClassLoader 的dexPath
参数中分割出来的某个存放文件的路径。在 Android 中,系统使用 PathClassLoader 来加载apk中的dex存放到Element数组中,因此apk中的classes.dex都是通过它来加载的。
总结
Android 中,apk 安装时,系统会使用 PathClassLoader 来加载apk文件中的dex,PathClassLoader的构造方法中,调用父类的构造方法,实例化出一个 DexPathList ,DexPathList 通过 makePathElements 在所有传入的dexPath 路径中,找到DexFile,存入 Element 数组,在应用启动后,所有的类都在 Element 数组中寻找,不会再次加载。
在热更新时,实现 DexClassLoader 子类,传入要更新的dex/apk/jar补丁文件路径(如sd卡路径中存放的patch.jar),通过反射拿到 DexPathList,得到补丁 Element 数组,再从Apk原本安装时使用的 PathClassLoader 中拿到旧版本的 Element 数组,合并新旧数组,将补丁放在数组最前面,这样一个类一旦在补丁 Element 中找到,就不会再次加载,这样就能替换旧 Element 中的旧类,实现热更新。
参考资料
http://androidxref.com/7.1.2_r36/
https://jaeger.itscoder.com/android/2016/08/27/android-classloader.html
https://blog.csdn.net/zhoushishang/article/details/38703623