这是 ZY 第 13 篇原创技术文章

预备知识

  1. 了解 android 基本 ClassLoader 知识

看完本文可以达到什么程度

  1. 了解 PathClassLoader 和 DexClassLoader 区别

文章概览

一、起因

说起 Android 中的 PathClassLoader 和 DexClassLoader,先提出一个疑问,PathClassLoader 和 DexClassLoader 有什么区别呢?
关于答案,我斗胆猜测一下,大家心中的回答一定是 PathClassLoader 是用来加载已经安装的 apk 的,DexClassLoader 是用来加载存储空间的 dex / apk 文件的。为什么这样说呢,因为之前我也一直这样理解的,而且网上大部分文章中也都是这样讲解的。
那为何突然又谈起 PathClassLoader 和 DexClassLoader 呢?起因是我在前段时间写了一些插件化的 demo,当时忘记了 PathClassLoader 和 DexClassLoader 这回事,直接用 PathClassLoader 去加载插件了,竟然也可以加载成功???一丝丝的困惑浮现在我英俊帅气的脸庞上,聪明的小脑瓜里打上了一个小小的问号。于是乎去翻了一下源码,就有了这篇文章。

二、先放结论

先放结论,PathClassLoader 和 DexClassLoader 都能加载外部的 dex/apk,只不过区别是 DexClassLoader 可以指定 optimizedDirectory,也就是 dex2oat 的产物 .odex 存放的位置,而 PathClassLoader 只能使用系统默认位置。但是这个 optimizedDirectory 在 Android 8.0 以后也被舍弃了,只能使用系统默认的位置了。

我们这里先基于 android 5.0 代码来分析,然后再看看其他系统版本的一些区别。(选取 5.0 是因为此时 art 的源码还比较简单~)

三、ClassLoader 的构造函数

3.1 BaseDexClassLoader 构造函数

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 构造函数有四个参数,含义如下:

  • dexPath: 需要加载的文件列表,文件可以是包含了 classes.dex 的 JAR/APK/ZIP,也可以直接使用 classes.dex 文件,多个文件用 “:” 分割
  • optimizedDirectory: 存放优化后的 dex,可以为空
  • libraryPath: 存放需要加载的 native 库的目录
  • parent: 父 ClassLoader

通过构造函数我们大概可以了解到 BaseDexClassLoader 的运行方式,传入 dex 文件,然后进行优化,保存优化后的 dex 文件到 optimizedDirectory 目录。

3.2 PathClassLoader 构造函数

接着我们再看 PathClassLoader 的构造函数。

/*** 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 为空。

3.3 DexClassLoader 构造函数

再来看看 DexClassLoader 的构造函数。和 BaseDexClassLoader 构造函数的参数是一样的。

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 参数的处理

既然知道了区别在 optimizedDirectory,那就来看看 BaseDexClassLoader 里是怎么处理 optimizedDirectory 的。

4.1 DexPathList 处理

在 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 里了。

4.2 DexFile 处理

其实在 DexFile.loadDex 里,也是直接调用了 DexFile 的构造函数

class DexFile {public DexFile(File file) throws IOException {this(file.getPath());}public DexFile(String fileName) throws IOException {// 调用 openDexFile 处理 dexmCookie = openDexFile(fileName, null, 0);mFileName = fileName;guard.open("close");}private DexFile(String sourceName, String outputName, int flags) throws IOException {// ...// 调用 openDexFile 处理 dexmCookie = 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 源码对照查看。

4.3 native 处理

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 有些改动。

5.1 Android 4.4

有了上面的分析基础,我们分析 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 文件的存储路径。

5.2 Android 8.0

在 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 directorydir += "/" + 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 的存储目录。

总结

关于我

谈谈 Android 中的 PathClassLoader 和 DexClassLoader相关推荐

  1. 谈谈Android中的HandlerThread

    一.简介 在详细讲解这篇文章之前,应该有部分的Android初级开发者并没用使用过HandlerThread,最常使用的消息传递机制应该离不开Handler,那么Handler跟HandlerThre ...

  2. 谈谈android中的内存泄漏

    写在前面 内存泄漏实际上很多时候,对于开发者来说不容易引起重视.因为相对于crash来说,android中一两个地方发生内存泄漏的时候,对于整体没有特别严重的影响.但是我想说的是,当内存泄漏多的时候, ...

  3. android 代码中使用dp,简单谈谈Android中SP与DP的区别

    从一开始写Android程序,就被告知这些常识 一.dp(或者dip device independent pixels) 一种基于屏幕密度的抽象单位.在每英寸160点的显示器上,1dp=1px.不同 ...

  4. android中sp的意义_简单谈谈Android中SP与DP的区别

    从一开始写android程序,就被告知这些常识 一.dp(或者dip device independent pixels) 一种基于屏幕密度的抽象单位.在每英寸160点的显示器上,1dp=1px.不同 ...

  5. 摩客怎么设置安卓的dp_简单谈谈Android中SP与DP的区别

    从一开始写Android程序,就被告知这些常识 一.dp(或者dip device independent pixels) 一种基于屏幕密度的抽象单位.在每英寸160点的显示器上,1dp=1px.不同 ...

  6. Android 中插件的编写方法

    java中插件主要使用反射机制来完成,Android与Java中一样也是通过反射机制,不同的是Android中使用的是DexClassLoader和PathClassLoader. 原因很简单,And ...

  7. 【Android 逆向】ART 脱壳 ( DexClassLoader 脱壳 | exec_utils.cc 中执行 Dex 编译为 Oat 文件的 Exec 和 ExecAndReturnC函数 )

    文章目录 前言 一.exec_utils.cc#Exec 函数分析 二.exec_utils.cc#ExecAndReturnCode 函数分析 前言 在上一篇博客 [Android 逆向]ART 脱 ...

  8. 【Android 逆向】ART 脱壳 ( DexClassLoader 脱壳 | oat_file_assistant.cc 中涉及的 oat 文件生成流程 )

    文章目录 前言 一.dalvik_system_DexFile.cc#DexFile_openDexFileNative 函数分析 二.oat_file_manager.cc#OpenDexFiles ...

  9. android属于数据库管理系统,详细谈谈Android系统中的SQLite数据库的应用

    数据库是按照数据结构来组织.存储和管理数据的仓库,而在信息话的社会,数据库又不单单仅限与数据的相关内容,现在数据库技术是管理信息系统.办公自动化系统.决策支持系统等各类信息系统的核心部分,而SQL是结 ...

最新文章

  1. web前端----jQuery属性操作
  2. Aspx页面中直接编写javascript脚本
  3. 关系数据库规范化理论(关系数据库设计理论)
  4. find_path、find_library备忘录
  5. HTTP、HTTPS、SSL、TLS之间的关系
  6. [React] 尚硅谷 -- 学习笔记(二)
  7. iOS 13发布日期确定 苹果WWDC2019定档!
  8. Oracle中表pagesize,Oracle 解决显示凌乱串行问题时column、pagesize、linesize的设定
  9. LeetCode(1046)——最后一块石头的重量(JavaScript)
  10. Spark SQL将rdd转换为数据集-反射来推断Inferring the Schema Using Reflection
  11. 2 机器学习基本概念
  12. android安全静态分析,[分享]发几个Android静态和动态分析的小技巧
  13. 货币转换python代码_在Python中创建货币转换器
  14. pg数据库开启远程连接_PostgreSQL 允许远程访问设置的操作
  15. 两招彻底驯服Vista任务管理器这头倔驴
  16. 机器学习——长短期记忆(LSTM)
  17. 使用dba_waiters检查锁等待
  18. Gitlab上传代码时报错文件太大无法上传
  19. 如何利用HackRF分析无线电信号,解读无线语言
  20. js判断对象类型的两种方法

热门文章

  1. 27.思科防火墙(ASA)
  2. WP8.1学习系列(第二十章)——添加控件和处理事件
  3. SQLALCHEMY_DATABASE_URI格式实例
  4. gd动态曲线 php_PHP GD库动态生成折线图的实例代码
  5. php 标点符号反转,PHP删除标点符号(无破折号)
  6. linux目录删除不释放空间,删除linux文件后,磁盘空间未释放的解决办法
  7. New Year and Ascent Sequence(二分)
  8. pSort CodeForces - 28B(并查集)
  9. 计算机编程英语用语,计算机编程英语词汇
  10. matlab armax 无法识别,求教:Java调用系统辨识工具箱内的armax函数出错