谈谈 Android 中的 PathClassLoader 和 DexClassLoader
【這是 ZY 第 13 篇原創技術文章】
預備知識
看完本文可以達到什么程度
文章概覽
一、起因
說起 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 參數。
我們再回顧一下調用的鏈路:
再繼續往下看,就走到了 native 邏輯。native 邏輯可以下載 art 源碼對照查看。
4.3 native 處理
openDexFileNative 對應的 native 邏輯在 dalvik_system_DexFile.cc 里的 DexFile_openDexFileNative 方法。
在 DexFile_openDexFileNative 里主要做事情是處理 DEX 文件,并生成 .odex 文件到 optimizedDirectory 里。
這里關于 optimizedDirectory 的處理路徑是:
在 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 獲取的。
GetDalvikCacheOrDie 獲取的就是 /data/dalvik-cache/ 目錄。
這里我們回顧一下之前提出的問題,避免迷失在茫茫代碼中。
我們的問題是 optimizedDirectory 參數傳空和不為空有什么區別,PathClassLoader 傳入的 optmizedDirectory 為空,而 DexClassLoader 傳入的 optimizedDirectory 是用戶自定義的目錄。
回看一下調用鏈路。
到這里我們就可以得出結論了,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的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怪物猎人物语恐暴龙基因什么搭配(如何评价
- 下一篇: c语言code用法_visual cod