談談 Android 中的 PathClassLoader 和 DexClassLoader

這是 ZY 第 13 篇原創技術文章java

預備知識

  1. 瞭解 android 基本 ClassLoader 知識

看完本文能夠達到什麼程度

  1. 瞭解 PathClassLoader 和 DexClassLoader 區別

文章概覽

sumamry

1、原由

提及 Android 中的 PathClassLoader 和 DexClassLoader,先提出一個疑問,PathClassLoader 和 DexClassLoader 有什麼區別呢?
關於答案,我斗膽猜想一下,你們心中的回答必定是 PathClassLoader 是用來加載已經安裝的 apk 的,DexClassLoader 是用來加載存儲空間的 dex / apk 文件的。爲何這樣說呢,由於以前我也一直這樣理解的,並且網上大部分文章中也都是這樣講解的。
那爲什麼忽然又談起 PathClassLoader 和 DexClassLoader 呢?原由是我在前段時間寫了一些插件化的 demo,當時忘記了 PathClassLoader 和 DexClassLoader 這回事,直接用 PathClassLoader 去加載插件了,居然也能夠加載成功???一絲絲的困惑浮如今我英俊帥氣的臉龐上,聰明的小腦瓜裏打上了一個小小的問號。因而乎去翻了一下源碼,就有了這篇文章。android

2、先放結論

先放結論,PathClassLoader 和 DexClassLoader 都能加載外部的 dex/apk,只不過區別是 DexClassLoader 能夠指定 optimizedDirectory,也就是 dex2oat 的產物 .odex 存放的位置,而 PathClassLoader 只能使用系統默認位置。可是這個 optimizedDirectory 在 Android 8.0 之後也被捨棄了,只能使用系統默認的位置了。app

咱們這裏先基於 android 5.0 代碼來分析,而後再看看其餘系統版本的一些區別。(選取 5.0 是由於此時 art 的源碼還比較簡單~)ide

3、ClassLoader 的構造函數

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 目錄。this

3.2 PathClassLoader 構造函數

接着咱們再看 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

3.3 DexClassLoader 構造函數

再來看看 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 傳入的是用戶指定的目錄。

4、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 處理 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 源碼對照查看。

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 是用戶自定義的目錄。
回看一下調用鏈路。

classloader

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、其餘系統版本上進行驗證

不過上述的分析是在 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 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 的存儲目錄。

總結

sumamry

關於我

about
相關文章
相關標籤/搜索