1. 程式人生 > >Android中ClassLoader和java中有什麼關係和區別

Android中ClassLoader和java中有什麼關係和區別

ClassLoader 簡介

對於 Java 程式來說,編寫程式就是編寫類,執行程式也就是執行類(編譯得到的 class 檔案),其中起到關鍵作用的就是類載入器 ClassLoader。

任何一個 Java 程式都是由若干個 class 檔案組成的一個完整的 Java 程式,在程式執行時,需要將 class 檔案載入到 JVM 中才可以使用,負責載入這些 class 檔案的就是 Java 的類載入(ClassLoader)機制。

因此 ClassLoader 的作用簡單來說就是載入 class 檔案,提供給程式執行時使用。

ClassLoader 的雙親委託模型(Parent Delegation Model
 )

先來看 jdk 中的 ClassLoader 類的構造方法,其需要傳入一個父類載入器,並持有該引用。

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}

當類載入器收到載入類或資源的請求時,通常都是先委託給父類載入器載入,也就是說只有當父類載入器找不到指定類或資源時,自身才會執行實際的類載入過程,具體的載入過程如下:

  1. 源 ClassLoader 先判斷該 Class 是否已載入,如果已載入,則直接返回 Class,如果沒有則委託給父類載入器。
  2. 父類載入器判斷是否載入過該 Class,如果已載入,則直接返回 Class,如果沒有則委託給祖父類載入器。
  3. 依此類推,直到始祖類載入器(引用類載入器)。
  4. 始祖類載入器判斷是否載入過該 Class,如果已載入,則直接返回 Class,如果沒有則嘗試從其對應的類路徑下尋找 class 位元組碼檔案並載入。如果載入成功,則直接返回 Class,如果載入失敗,則委託給始祖類載入器的子類載入器。
  5. 始祖類載入器的子類載入器嘗試從其對應的類路徑下尋找 class 位元組碼檔案並載入。如果載入成功,則直接返回 Class,如果載入失敗,則委託給始祖類載入器的孫類載入器。
  6. 依此類推,直到源 ClassLoader。
  7. 源 ClassLoader 嘗試從其對應的類路徑下尋找 class 位元組碼檔案並載入。如果載入成功,則直接返回 Class,如果載入失敗,源 ClassLoader 不會再委託其子類載入器,而是丟擲異常。

如果需要詳細瞭解 ClassLoader 的資訊,可以藉助以下文章深入瞭解:

Android 中的 ClassLoader

Android 的 Dalvik/ART 虛擬機器如同標準 Java 的 JVM 虛擬機器一樣,也是同樣需要載入 class 檔案到記憶體中來使用,但是在 ClassLoader 的載入細節上會有略微的差別。

Android 中的 dex 檔案

Android 應用打包成 apk 檔案時,class 檔案會被打包成一個或者多個 dex 檔案。將一個 apk 檔案字尾改成 .zip 格式解壓後(也可以直接解壓,apk 檔案本質是個 zip 檔案),裡面就有 class.dex 檔案,由於 Android 的 65K 問題(不要糾結是 64K 還是 65K),使用 MultiDex 就會生成多個 dex 檔案。

當 Android 系統安裝一個應用的時候,會針對不同平臺對 Dex 進行優化,這個過程由一個專門的工具來處理,叫 DexOpt 。DexOpt 是在第一次載入 Dex 檔案的時候執行的,該過程會生成一個 ODEX 檔案,即 Optimised Dex。執行 ODEX 的效率會比直接執行 Dex 檔案的效率要高很多,加快 App 的啟動和響應。

ODEX 相關的細節可以閱讀以下文章擴充套件:

注:本人的 5.0 機器 ODEX 優化後的檔案是在 /data/dalvilk-cache 資料夾下的,6.0 機器該資料夾下只有 framework 和部分內建的 App 的優化後的 dex 檔案,查詢相關資料後沒有找到明確的說法,目前猜測和 ROM 有關係,後續再深究下這個問題。

總之,Android 中的 Dalvik/ART 無法像 JVM 那樣 直接 載入 class 檔案和 jar 檔案中的 class,需要通過 dx 工具來優化轉換成 Dalvik byte code 才行,只能通過 dex 或者 包含 dex 的jar、apk 檔案來載入(注意 odex 檔案字尾可能是 .dex 或 .odex,也屬於 dex 檔案),因此 Android 中的 ClassLoader 工作就交給了 BaseDexClassLoader 來處理。

注:如果 jar 檔案包含有 dex 檔案,此時 jar 檔案也是可以用來載入的,不過實際載入的還是其中的 dex 檔案,不要弄混淆了。

BaseDexClassLoader 及其子類

在 Android 開發者官網上的 ClassLoader 的文件說明中我們可以看到,ClassLoader 是個抽象類,其具體實現的子類有 BaseDexClassLoader 和SecureClassLoader 。

SecureClassLoader 的子類是 URLClassLoader ,其只能用來載入 jar 檔案,這在 Android 的 Dalvik/ART 上沒法使用的。

BaseDexClassLoader 的子類是 PathClassLoader 和 DexClassLoader 。

PathClassLoader

PathClassLoader 在應用啟動時建立,從 data/app/… 安裝目錄下載入 apk 檔案。

其有 2 個建構函式,如下所示,這裡遵從之前提到的雙親委託模型:

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath,
        ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}
  • dexPath : 包含 dex 的 jar 檔案或 apk 檔案的路徑集,多個以檔案分隔符分隔,預設是“:”
  • libraryPath : 包含 C/C++ 庫的路徑集,多個同樣以檔案分隔符分隔,可以為空

PathClassLoader 裡面除了這 2 個構造方法以外就沒有其他的程式碼了,具體的實現都是在 BaseDexClassLoader 裡面,其 dexPath 比較受限制,一般是已經安裝應用的 apk 檔案路徑。

在 Android 中,App 安裝到手機後,apk 裡面的 class.dex 中的 class 均是通過 PathClassLoader 來載入的。

我們可以新建一個專案來驗證下,在 MainActivity 中新增如下程式碼:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ClassLoader loader = MainActivity.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
        }
    }
}

輸出結果是:

 I/System.out: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.jaeger.testclassloader-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
 I/System.out: [email protected]

/data/app/com.jaeger.testclassloader-2/base.apk 就是示例應用安裝在手機上的位置。

BootClassLoader 是 PathClassLoader 的父載入器,其在系統啟動時建立,在 App 啟動時會將該物件傳進來,具體的呼叫在com.android.internal.os.ZygoteInit 的 main() 方法中呼叫了 preload() , 然後呼叫 preloadClasses() 方法,在該方法內部呼叫了 Class 的 forName() 方法:

Class.forName(line, true, null);

forName() 方法原始碼如下,方法內部獲取到 BootClassLoader 例項:

public static Class<?> forName(String className, boolean shouldInitialize,
        ClassLoader classLoader) throws ClassNotFoundException {
    if (classLoader == null) {
        classLoader = BootClassLoader.getInstance();
    }
    // Catch an Exception thrown by the underlying native code. It wraps
    // up everything inside a ClassNotFoundException, even if e.g. an
    // Error occurred during initialization. This as a workaround for
    // an ExceptionInInitializerError that's also wrapped. It is actually
    // expected to be thrown. Maybe the same goes for other errors.
    // Not wrapping up all the errors will break android though.
    Class<?> result;
    try {
        result = classForName(className, shouldInitialize, classLoader);
    } catch (ClassNotFoundException e) {
        Throwable cause = e.getCause();
        if (cause instanceof LinkageError) {
            throw (LinkageError) cause;
        }
        throw e;
    }
    return result;
}

而 PathClassLoader 的例項化又是在哪進行的呢?在原始碼中尋找下其構造方法呼叫的地方,結果如下:

其中:

  • 在 ZygoteInit 中的呼叫是用來啟動相關的系統服務

  • 在 ApplicationLoaders 中用來載入系統安裝過的 apk,用來載入 apk 內的 class ,其呼叫是在 LoadApk 類中的 getClassLoader() 方法中呼叫的,得到的就是 PathClassLoader:

    mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
            mBaseClassLoader);
    
DexClassLoader

介紹 DexClassLoader 之前,先來看看其官方描述:

A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry. This can be used to execute code notinstalled as part of an application.

很明顯,對比 PathClassLoader 只能載入已經安裝應用的 dex 或 apk 檔案,DexClassLoader 則沒有此限制,可以從 SD 卡上載入包含 class.dex 的 .jar 和 .apk 檔案,這也是外掛化和熱修復的基礎,在不需要安裝應用的情況下,完成需要使用的 dex 的載入。

DexClassLoader 的原始碼裡面只有一個構造方法,這裡也是遵從雙親委託模型:

public DexClassLoader(String dexPath, String optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

引數說明:

  • String dexPath : 包含 class.dex 的 apk、jar 檔案路徑 ,多個用檔案分隔符(預設是 :)分隔

  • String optimizedDirectory : 用來快取優化的 dex 檔案的路徑,即從 apk 或 jar 檔案中提取出來的 dex 檔案。該路徑不可以為空,且應該是應用私有的,有讀寫許可權的路徑(實際上也可以使用外部儲存空間,但是這樣的話就存在程式碼注入的風險),可以通過以下方式來建立一個這樣的路徑:

    File dexOutputDir = context.getCodeCacheDir();
    

    注:後續發現,getCodeCacheDir() 方法只能在 API 21 以上可以使用。

  • String libraryPath : 儲存 C/C++ 庫檔案的路徑集

  • ClassLoader parent : 父類載入器,遵從雙親委託模型

簡單介紹了 PathClassLoader 和 DexClassLoader,但這兩者都是對 BaseDexClassLoader 的一層簡單封裝,真正的實現都在 BaseClassLoader 內。

BaseClassLoader 原始碼分析

先來看一眼 BaseClassLoader 的結構:

其中有個重要的欄位 private final DexPathList pathList ,其繼承 ClassLoader 實現的 findClass() 、findResource() 均是基於 pathList 來實現的(省略了部分原始碼):

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        ...
        return c;
    }
    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }
    @Override
    protected Enumeration<URL> findResources(String name) {
        return pathList.findResources(name);
    }
    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

那麼重要的部分則是在 DexPathList 類的內部了,DexPathList 的構造方法也較為簡單,和之前介紹的類似:

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
}

接受之前傳進來的包含 dex 的 apk/jar/dex 的路徑集、native 庫的路徑集和快取優化的 dex 檔案的路徑,然後呼叫 makePathElements() 方法生成一個Element[] dexElements 陣列,Element 是 DexPathList 的一個巢狀類,其有以下欄位:

static class Element {
	private final File dir;
	private final boolean isDirectory;
	private final File zip;
	private final DexFile dexFile;
	private ZipFile zipFile;
	private boolean initialized;
}

makePathElements() 是如何生成 Element 陣列的?繼續看原始碼:

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions) {
    List<Element> elements = new ArrayList<>();
    // 遍歷所有的包含 dex 的檔案
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();
        // 判斷是不是 zip 型別
        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            // 如果是資料夾,則直接新增 Element,這個一般是用來處理 native 庫和資原始檔
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()) {
            // 直接是 .dex 檔案,而不是 zip/jar 檔案(apk 歸為 zip),則直接載入 dex 檔案
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                // 如果是 zip/jar 檔案(apk 歸為 zip),則將 file 值賦給 zip 欄位,再載入 dex 檔案
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }