Android 插件化基礎——ClassLoader 源碼解析

其他有關插件化的文章歡迎大家觀閱
插件化踩坑之路——Small和Atlas方案對比
Android插件化基礎篇—— class 文件
Android插件化基礎篇 — dex 文件
Android 插件化基礎——虛擬機

Android 和 Java 平臺的類加載平臺區別較大,是我們基礎篇的重點,我們將從三個方面來講解 ClassLoader。

Java 中的 ClassLoader 回顧

Java平臺的ClassLoader

之前的文章中,我們已經看過這張圖了,那篇文章中也簡單的講解了類的加載流程,加載流程兩個平臺差不多,如何大家還不太熟悉可以去上面給出的虛擬機文章中再復習一下。

Android 中的 ClassLoader 詳解

Android 中的 ClassLoader 種類

Android 中的 ClassLoader 有以下幾種類型:

  • BootClassLoader
  • PathClassLoader
  • DexClassLoader
  • BaseDexClassLoader

BootClassLoader 作用和 Java 中的 Bootstrap ClassLoader 作用是類似的,是用來加載 Framework 層的字節碼文件的。

PathClassLoader 作用和 Java 中的 App ClassLoader 作用有點類似,用來加載已經安裝到系統中的 APK 文件中的 Class 文件。

DexClassLoader 和 Java 中的 Custom ClassLoader 作用類似,用來加載指定目錄中的字節碼文件。

BaseDexClassLoader 是一個父類,DexClassLoader 和 PathClassLoader 都是它的子類。

一個 App 至少需要 BootClassLoader 和 PathClassLoader 才能運行。為了證明這一點,我們寫一個簡單的頁面,在 MainActivityonCreate() 方法中寫下如下代碼:

 ClassLoader classLoader = getClassLoader();
        if (classLoader != null) {
            Log.e("weaponzhi", "classLoader: " + classLoader.toString());

            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.e("weaponzhi","classLoader: "+classLoader.toString());
            }
        }

最后我們發現輸出dalvik.system.PathClassLoaderjava.lang.BootClassLoader。當然不同機子可能輸出的結果不同,但至少會有這兩個 ClassLoader。BootClassLoader 負責加載 framework 字節碼文件,所以每個應用都是需要的,而 PathClassLoader 用來加載已安裝 Apk 的字節碼文件,這些東西都是一個應用啟動的必要東西。

Android 中 ClassLoader 特點及作用

Android 中的 ClassLoader 最大的特點就是雙親代理模型。雙親代理模型主要分三個過程:在加載字節碼的時候,會詢問當前 ClassLoader 是否已經加載過,如果加載過則直接返回,不再重復加載,如果沒有的話,會查詢 parent 是否加載過,如果加載過,就直接返回 parent 加載的字節碼文件。如果整個繼承線路上的 ClassLoader 都沒有加載,執行類才會由當前 ClassLoader 類進行真正加載。

這樣做的好處是,如果一個類被位于樹中任意 ClassLoader 節點加載過,那么以后整個系統生命周期中,這個類都將不會被加載,大大提高了加載類的效率。由于這樣的特點,就給我們 ClassLoader 帶來了兩個作用。

第一個作用就是類加載的共享功能。當一個 framework 層中的類被頂層 ClassLoader 加載過,那么這個類就會被緩存在內存里,以后任何需要用到底地方都不會重新加載了。

第二個作用就是類加載的隔離功能。不同繼承路線上的 ClassLoader 加載的類肯定不是同一個類,這樣就有一定的安全性,避免了用戶自己寫一些代碼冒充核心類庫來訪問這些類庫中核心代碼和變量。

所以如何判斷兩個類是同一個類呢,不僅需要工程中的包名類名一致,還需要由同一個 ClassLoader 加載的,這三條同時滿足才能說是一個類。

Android ClassLoader 源碼講解

我們下面就來通過源碼來看看 Android ClassLoader 到底是如何實現雙親代理模式的。

首先我們進入 ClassLoader.java 這個類,查找它最核心的方法 loadClass() 看看它是怎么實現的

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 1.查看 class 是否已經被加載過
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
            //2.如果沒有被加載過,則判斷 parent ClassLoader 有沒有加載過
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
            //3.如果類沒有被加載過,那么就通過當前 ClassLoader 來加載
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

我在代碼中注釋已經比較清楚了,源碼中首先會判斷當前的 ClassLoader 有沒有加載過這個類,如果沒有加載過,再會看看 parent ClassLoader 有沒有加載過,如果整個繼承線路走過后 class 依然為 null,則再回到當前 ClassLoader 通過 findClass() 方法來加載 class。

好,現在讓我們繼續跟蹤 findClass()方法,進去后發現這個方法是個空實現,說明真正的實現代碼都在 ClassLoader 的子類中實現,我們在 Android Studio 中,查找類似 PathClassLoader 這樣的類是無法看到代碼的,所以我們可以通過源碼網站 AndroidXRef 或者其他觀看源碼的方式來查看下 Android 幾個 ClassLoader 的具體實現。

打開 DexClassLoader發現很簡單,類中只有一個構造方法,繼承自 BaseDexClassLoader,下面我們來看看這個構造方法。

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

參數dexpath指定我們要加載的 dex 文件路徑,optimizedDirectory指定該 dex 文件要被拷貝到哪個路徑中,一般是應用程序內部路徑。

DexClassLoader類上有一段官方注釋:

A class loader that loads classes from {@code .jar} and {@code .apk} files containing a {@code classes.dex} entry. This can be used to execute code not installed as part of an application.

這段注釋的意思就是,DexClassLoader 可以加載一些 jar 包和 apk 包里面的 dex 文件,可以用來加載一些并沒有安裝到系統應用中的類。所以,DexClassLoader 是動態加載的核心

下面我們再來看看 PathClassLoader 是如何實現的,它同樣也是繼承于 BaseDexClassLoader,并且也重寫了構造方法。

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

我們可以看到,它和 DexClassLoader 的區別就在于少了一個 optimizedDirectory 的參數,所以 PathClassLoader 沒有辦法加載沒有安裝到系統中的應用的類。

我們發現,這兩個 ClassLoader 并沒有什么具體實現,真正的實現都是在他們的父類 BaseDexClassLoader中,所以我們下面看一下它的實現。

public class BaseDexClassLoader extends ClassLoader{
private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath,File optimizedDirectory,
            String libraryPath,ClassLoader parent){
        super(parent);
        this.pathList = new DexPathList(this,dexPath,libraryPath,optimizedDirectory);       
    }

    @Override
    protected Class<?> findClass(String name) throw ClassNotFoundException{
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name,suppressedExceptions);
        if (c == null){
            ClassNotFoundException cnfe = new ClassNorFoundException("xxx");
            for (Throwable t : suppressedExceptions){
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
}

我們通過構造方法可以觀察到,如果 optimizedDirectory 為空,那么代表這是 PathClassLoader,不為空則是 DexClassLoader,findClass()方法雖然我們終于看到了實現,但發現真正的實現還沒有在這里,而是在 DexPathList對象的findClass()方法中,不要氣餒,結果就在前方,我們繼續跟進!

DexPathList這個類代碼比較多,我們來從它的成員變量中開始,挑重點看。

final class DexPathList{
    private static final String DEX_SUFFIX = ".dex";
    private final ClassLoader definingContext;
    private final Element[] dexElements;
    ...
    public DexPathList(ClassLoader definingContext,
        String dexPath,String libraryPath,File optimizedDirectory){
        ...
        this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory,suppressedException);
        ...
    }

    public Class findClass(String name,List<Throwable> suppressed){
        for (Element element : dexElements){
            DexFile dex = element.dexFile;

            if(dex != null){
                Class clazz = dex.loadClassBinaryName(name,definingContext,suppressed);
                if(clazz != null){
                    return clazz;
                }
            }
        }
    }
}

我們關注幾個點,一個是 DEX_SUFFIX 這個成員變量,代表 dex 文件后綴,方便后面的一些文件處理判斷使用。 definingContext 就是在初始化的時候傳進來的 ClassLoader,dexElements DexPathList 中一個靜態內部類對象數組,在構造方法中初始化,這個對象數組是 findClass() 的關鍵參數,通過遍歷獲取 Elements 中的 DexFile 對象,調用 DexFile 的 loadClassBinaryName() 方法,完成 class 文件的獲取。

static class Element{
    private final File file;
    private final boolean isDirectory;
    private final File zip;
    private final DexFile dexFile;

    public Element(File file,boolean isDirectory,File zip,DexFile dexFile){
        this.dir = dir;
        this.isDirectory = isDirectory;
        this.zip = zip;
        this.dexFIle = dexFIle;
    }
}

Element 就是 dexElements 對象數組存儲的具體靜態內部類,該類我只是簡單列舉下它的成員變量。dexElements 在 DexPathList 的構造方法中初始化,我們來細致的看下 makeDexElements 方法,該方法直接指向 makeElements()方法,源碼如下:

private static Element[] makeElements(List<File> files,File optimizedDirectory,
                                      List<IOException> suppressedExceptions,
                                      boolean ignoreDexFiles,
                                      ClassLoader loader){
        Element[] elements = new Element[file.size()];
        int elementsPos = 0;
        for (File file : files){
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();
            //1
            if (path.contains(zipSeparator)){
                ...
            //2
            }else if(file.isDirectory()){
                elements[elementsPos++] == new Element(file,true,null,null);
            //3
            }else if (file.isFile()){
                //4
                if(!ignoreDexFiles && name.endsWith(DEX_SUFFIX)){
                    dex = loadDexFile(file,optimizedDirectory,loader,elements);
                //5
                }else{
                    zip = file;
                    //6
                    if(!ignoreDexFiles){
                        dex = loadDexFile(file,optimizedDirectory,loader,elements);
                    }
                }
            }
        }                                     
}

這里我省略掉了一些代碼,只看重點。其中注釋中第一個和第二個 if 語句中的代碼的作用是如果路徑是文件夾的話,就繼續向下遞歸,第三個判斷是否是文件,如果是,進入第四個,判斷文件是否是以 .dex 為后綴的,如果是的話標明這個文件就是我們需要加載的 dex 文件,通過 loadDexFile() 方法來加載 DexFile 對象。如果是文件,并且是個壓縮文件的話,就會進入第五個 if 語句中,同樣會通過 loadDexFile() 來進行 DexFile 加載。下面來看一下 loadDexFile() 方法實現。

private static DexFile loadDexFile(File file,File optimizedDirectory,Classloader loader,
                                   Element[] elements) throw IOException{
        if(optimizedDirectory == null){
            return new DexFile(file,loader,elements);
        }else{
            String optimizedPath = optimizedPathFor(file,optimizedDirectory);
        }
}

如果optimizedDirectory為空,說明文件就是 dex 文件,那么直接創建 DexFile 對象即可,如果不為空,則調用 loadDex() 方法,將它解壓然后獲取內部真正的 DexFile。所以 makeElements() 就是通過文件獲取 dex 文件,轉化為 Elements 對象數組,然后給findClass() 方法使用。

loadClassBinaryName()方法再往下走就是 native 方法了,我們就無法繼續看了,大概可以想像這個 native 方法就是通過 C、C++去查找 dex 指定 name 相關的東西,然后將它拼成 class 字節碼,最后返回給我們。

整體的源碼我們大概就看過了,實際上不是很復雜,只是嵌套很多,真正復雜的地方都在 native 中了,所以我們看源碼一定要耐心細心,不能懼怕,看不懂就多看幾遍,學習一下他們的編程思路和設計思想,對我們能力提高有極大幫助。

Android 中的動態加載比 Java 程序復雜在哪里

Android 中的動態加載在我們之前源碼分析之后,感覺看起來不是很復雜,只要利用好幾個 ClassLoader ,整體的思路還是比較清晰的,但在實際設計的時候遠遠沒有那么簡單,主要是因為 Android 有他的復雜性:

  • 有許多組件類,比如四大組件,都是需要注冊才能使用的。需要在 AndoridManifest 注冊才能使用。
  • 資源的動態加載非常復雜。Android 的資源很特殊,都是通過 id 注冊的,通過 id 從 Resource 實例中獲取對應的資源,如果是動態加載的新類,資源 id 就會找不到,總而言之就是資源也是需要動態注冊的
  • Android 每個版本對于類和資源加載的方式都是不同的,適配也是一個極為頭疼的問題。

以上難點總結起來可以用一句話概括:「Android 程序運行需要一個上下文環境」。上下文環境可以給組件提供需要的功能,比如主題、資源、查詢組件等。那么我們如何給動態加載的組件和類提供上下文環境呢,其實這就是第三方動態加載庫主要解決的問題,也是非常復雜的,像 Tinker 和 Atlas 這些比較成熟的動態加載方案都是以解決這些問題作為核心而設計的,我們個人要解決可能比較困難,但我們可以通過使用和閱讀源碼,來學習他們的實現原理,大致了解即可。


下一篇文章我們將利用我們學到的 ClassLoader 相關知識,自己嘗試寫一個簡單的插件加載 demo 和插件管理器。

本文部分內容參考于慕課網實戰課程「Android 應用發展趨勢必備武器 熱修復與插件化」,有興趣的朋友可以付費學習。
插件化實戰課程

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容