【这是 ZY 第 13 篇原创技术文章】java
提及 Android 中的 PathClassLoader 和 DexClassLoader,先提出一个疑问,PathClassLoader 和 DexClassLoader 有什么区别呢?
关于答案,我斗胆猜想一下,你们心中的回答必定是 PathClassLoader 是用来加载已经安装的 apk 的,DexClassLoader 是用来加载存储空间的 dex / apk 文件的。为何这样说呢,由于以前我也一直这样理解的,并且网上大部分文章中也都是这样讲解的。
那为什么忽然又谈起 PathClassLoader 和 DexClassLoader 呢?原由是我在前段时间写了一些插件化的 demo,当时忘记了 PathClassLoader 和 DexClassLoader 这回事,直接用 PathClassLoader 去加载插件了,居然也能够加载成功???一丝丝的困惑浮如今我英俊帅气的脸庞上,聪明的小脑瓜里打上了一个小小的问号。因而乎去翻了一下源码,就有了这篇文章。android
先放结论,PathClassLoader 和 DexClassLoader 都能加载外部的 dex/apk,只不过区别是 DexClassLoader 能够指定 optimizedDirectory,也就是 dex2oat 的产物 .odex 存放的位置,而 PathClassLoader 只能使用系统默认位置。可是这个 optimizedDirectory 在 Android 8.0 之后也被舍弃了,只能使用系统默认的位置了。app
咱们这里先基于 android 5.0 代码来分析,而后再看看其余系统版本的一些区别。(选取 5.0 是由于此时 art 的源码还比较简单~)ide
PathClassLoader 和 DexClassLoader 都是继承了 BaseDexClassLoader,这里先看一下。 BaseDexClassLoader 的构造函数。函数
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
/** * Constructs an instance. * * @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; may 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 BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
}
复制代码
BaseDexClassLoader 构造函数有四个参数,含义以下:优化
经过构造函数咱们大概能够了解到 BaseDexClassLoader 的运行方式,传入 dex 文件,而后进行优化,保存优化后的 dex 文件到 optimizedDirectory 目录。this
接着咱们再看 PathClassLoader 的构造函数。google
/** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). */
public class PathClassLoader extends BaseDexClassLoader {
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: * * <ul> * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. * <li>Raw ".dex" files (not inside a zip file). * </ulyanzheng> * * 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);
}
}
复制代码
关于 PathClassLoader 有一点稍微注意一下,代码注释中对 PathClassLoader 的介绍是,用来操做文件系统上的一系列文件和目录 的 ClassLoader 实现。其中并无提到只能加载安装后的 apk 文件。
PathClassLoader 有两个构造函数,区别在于传给 BaseDexClassLoader 的 libraryPath 是否为空。最终调用 BaseDexClassLoader 构造函数时,传入的 optimizedDirectory 为空。spa
再来看看 DexClassLoader 的构造函数。和 BaseDexClassLoader 构造函数的参数是同样的。.net
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 librarySearchPath 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 librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
复制代码
经过上面对构造函数的分析,咱们能够明白,PathClassLoader 和 DexClassLoader 关键不一样点,在 optimizedDirectory 参数上,PathClassLoader 传入的是 null,而 DexClassLoader 传入的是用户指定的目录。
既然知道了区别在 optimizedDirectory,那就来看看 BaseDexClassLoader 里是怎么处理 optimizedDirectory 的。
在 BaseDexClassLoader 里,直接将 optimizedDirectory 透传给了 DexPathList。 这里先简单介绍一下 DexPathList。 DexPathList 里有两个成员变量,dexElements 用来保存 dex 和资源列表,nativeLibraryDirectories 用来保存 native 库列表。
class DexPathList {
private final Element[] dexElements;
private final File[] nativeLibraryDirectories;
}
复制代码
在 DexPathList 中,使用 optimizedDirectory 的路径是:
DexPathList -> makeDexElements -> loadDexFile
复制代码
这里要看一下 loadDexFile 方法。
class DexPathList {
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
}
复制代码
在 DexPathList 中,会为每个 DEX 文件建立一个 DexFile 对象,建立方式有两种,optimizedDirectory 为空时,调用 DexFile(file) 建立,不然调用 DexFile.loadDex()。
这样对于 optimizedDirectory 的处理就流转到 DexFile 里了。
其实在 DexFile.loadDex 里,也是直接调用了 DexFile 的构造函数
class DexFile {
public DexFile(File file) throws IOException {
this(file.getPath());
}
public DexFile(String fileName) throws IOException {
// 调用 openDexFile 处理 dex
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
}
private DexFile(String sourceName, String outputName, int flags) throws IOException {
// ...
// 调用 openDexFile 处理 dex
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
}
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
// 最终调用 native 方法
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null) ? null : new File(outputName).getAbsolutePath(),
flags);
}
private static native long openDexFileNative(String sourceName, String outputName, int flags);
}
复制代码
DexFile 代码很少,上面基本上就是主要代码了。咱们能够看到,无论调用 DexFile 哪一个构造函数,最后都会经过 openDexFileNative 进行处理,区别就在于 outputName 参数是否为空,而 outputName 参数,就是上面一路传递下来的 optimizeDirectory 参数。
咱们再回顾一下调用的链路:
PathClassLoader.constructor / DexClassLoader.constructor -> BaseDexClassLoader.constructor -> DexPathList.constructor -> DexPathList.makeDexElements -> DexPathList.loadDexFile -> DexFile.constructor / DexFile.loadDex -> DexFile.openDexFile -> DexFile.openDexFileNative
复制代码
再继续往下看,就走到了 native 逻辑。native 逻辑能够下载 art 源码对照查看。
openDexFileNative 对应的 native 逻辑在 dalvik_system_DexFile.cc 里的 DexFile_openDexFileNative 方法。
在 DexFile_openDexFileNative 里主要作事情是处理 DEX 文件,并生成 .odex 文件到 optimizedDirectory 里。
这里关于 optimizedDirectory 的处理路径是:
DexFile_openDexFileNative -> ClassLinker::OpenDexFilesFromOat
复制代码
在 OpenDexFilesFromOat 里有这样一段处理逻辑:
ClassLinker::OpenDexFilesFromOat() {
// ...
if (oat_location == nullptr) {
// 若是 oat_location 为空,就使用默认的 dalvikcache
const std::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)));
cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());
oat_location = cache_location.c_str();
}
// ...
if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
// Create the oat file.
open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
oat_location, error_msgs));
}
}
复制代码
上面方法里的 oat_location 就是 optimizeDirectory 传入到 native 中的化身。这里有一个判断逻辑,若是 oat_location 为空的话,就采用默认的 dalvikcache 路径。以后调用 CreateOatFileForDexLocation 去优化 DEX 文件了。
而 dalvikcache 是经过 GetDalvikCacheOrDie 获取的。
// art/runtime/utils.cc
std::string GetDalvikCacheOrDie(const char* subdir, const bool create_if_absent) {
CHECK(subdir != nullptr);
// 这里的 AndroidData 就是 /data 目录
const char* android_data = GetAndroidData();
const std::string dalvik_cache_root(StringPrintf("%s/dalvik-cache/", android_data));
const std::string dalvik_cache = dalvik_cache_root + subdir;
if (create_if_absent && !OS::DirectoryExists(dalvik_cache.c_str())) {
// Don't create the system's /data/dalvik-cache/... because it needs special permissions.
if (strcmp(android_data, "/data") != 0) {
int result = mkdir(dalvik_cache_root.c_str(), 0700);
if (result != 0 && errno != EEXIST) {
PLOG(FATAL) << "Failed to create dalvik-cache directory " << dalvik_cache_root;
return "";
}
result = mkdir(dalvik_cache.c_str(), 0700);
if (result != 0) {
PLOG(FATAL) << "Failed to create dalvik-cache directory " << dalvik_cache;
return "";
}
} else {
LOG(FATAL) << "Failed to find dalvik-cache directory " << dalvik_cache;
return "";
}
}
return dalvik_cache;
}
复制代码
GetDalvikCacheOrDie 获取的就是 /data/dalvik-cache/ 目录。
这里咱们回顾一下以前提出的问题,避免迷失在茫茫代码中。
咱们的问题是 optimizedDirectory 参数传空和不为空有什么区别,PathClassLoader 传入的 optmizedDirectory 为空,而 DexClassLoader 传入的 optimizedDirectory 是用户自定义的目录。
回看一下调用链路。
PathClassLoader.constructor / DexClassLoader.constructor -> BaseDexClassLoader.constructor -> DexPathList.constructor -> DexPathList.makeDexElements -> DexPathList.loadDexFile -> DexFile.constructor / DexFile.loadDex -> DexFile.openDexFile -> DexFile.openDexFileNative -> DexFile_openDexFileNative -> ClassLinker::OpenDexFilesFromOat
复制代码
到这里咱们就能够得出结论了,optmizedDirectory 不为空时,使用用户定义的目录做为 DEX 文件优化后产物 .odex 的存储目录,为空时,会使用默认的 /data/dalvik-cache/ 目录。
因此印证了开头的结论,PathClassLoader 其实并非只能加载安装后的 APK,也能够加载其余 DEX/JAR/APK 文件,只不过生成的 .odex 文件只能存储在系统默认路径下。
被误导多年的谜题终于解开了。耳边不由响起柯南破案的 BGM。
不过上述的分析是在 5.0 源码下进行的,咱们再选取 4.4 和 8.0 看一下。
为何选取这两个版本呢?首先 4.4 和 5.0 是 ART 和 Dalvik 的分水岭,而 8.0 之后对 PathClassLoader 有些改动。
有了上面的分析基础,咱们分析 4.4 的代码就顺畅的多了。一路从 Java 分析到 native。 Java 层代码没有什么变更,native 的入口仍是 DexFile_openDexFileNative。以后的代码就有了些许不同。
DexFile_openDexFileNative() {
// ...
if (outputName.c_str() == NULL) {
dex_file = linker->FindDexFileInOatFileFromDexLocation(dex_location, dex_location_checksum);
} else {
std::string oat_location(outputName.c_str());
dex_file = linker->FindOrCreateOatFileForDexLocation(dex_location, dex_location_checksum, oat_location);
}
// ...
}
复制代码
这里和 5.0 的区别就是 根据 outputName 也就是 optimizedDirectory 是否为空,调用了两个不一样的函数。 而 FindDexFileInOatFileFromDexLocation 里的逻辑就又有些熟悉了。
ClassLinker::FindDexFileInOatFileFromDexLocation() {
// ...
std::string oat_cache_filename(GetDalvikCacheFilenameOrDie(dex_location));
return FindOrCreateOatFileForDexLocationLocked(dex_location, dex_location_checksum, oat_cache_filename);
}
复制代码
默认也是获取到 dalvikcache 目录做为 .odex 文件的存储路径。
在 8.0 系统上,事情发生了一些微弱的变化,咱们看看 BaseDexClassLoader 的构造函数。
class BaseDexClassLoader {
/** * Constructs an instance. * Note that all the *.jar and *.apk files from {@code dexPath} might be * first extracted in-memory before the code is loaded. This can be avoided * by passing raw dex files (*.dex) in the {@code dexPath}. * * @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 this parameter is deprecated and has no effect * @param librarySearchPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
}
复制代码
一个很明显的变化就是,optimizedDirectory 被弃用了,传给 DexPathList 的 optimizedDirectory 直接为空,无论外面传进来什么值。 也就是说,在 8.0 上,PathClassLoader 和 DexClassLoader 其实已经没有什么区别了。DexClassLoader 也不能指定 optimizedDirectory 了。
而在 DexFile_openDexFileNative 中,能够看到,javaOutputName 参数也已经被弃用了。
static jobject DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName ATTRIBUTE_UNUSED, jint flags ATTRIBUTE_UNUSED, jobject class_loader, jobjectArray dex_elements) {
}
复制代码
以后对 DEX 文件的处理链路以下:
DexFile_openDexFileNative -> DexLocationToOdexNames -> OatFileManager::OpenDexFilesFromOat -> OatFileAssistant::OatFileAssistant -> OatFileAssistant::DexLocationToOdexFilename -> DexLocationToOdexNames
复制代码
在 DexLocationToOdexNames 方法里,对 .odex 文件的路径作了处理。
static bool DexLocationToOdexNames(const std::string& location, InstructionSet isa, std::string* odex_filename, std::string* oat_dir, std::string* isa_dir, std::string* error_msg) {
CHECK(odex_filename != nullptr);
CHECK(error_msg != nullptr);
// The odex file name is formed by replacing the dex_location extension with
// .odex and inserting an oat/<isa> directory. For example:
// location = /foo/bar/baz.jar
// odex_location = /foo/bar/oat/<isa>/baz.odex
// Find the directory portion of the dex location and add the oat/<isa>
// directory.
size_t pos = location.rfind('/');
if (pos == std::string::npos) {
*error_msg = "Dex location " + location + " has no directory.";
return false;
}
std::string dir = location.substr(0, pos+1);
// Add the oat directory.
dir += "oat";
if (oat_dir != nullptr) {
*oat_dir = dir;
}
// Add the isa directory
dir += "/" + std::string(GetInstructionSetString(isa));
if (isa_dir != nullptr) {
*isa_dir = dir;
}
// Get the base part of the file without the extension.
std::string file = location.substr(pos+1);
pos = file.rfind('.');
if (pos == std::string::npos) {
*error_msg = "Dex location " + location + " has no extension.";
return false;
}
std::string base = file.substr(0, pos);
*odex_filename = dir + "/" + base + ".odex";
return true;
}
复制代码
看到上面的处理就是在 DEX 文件同级目录下添加一个 oat/ 文件做为 .odex 的存储目录。