Android 插件化基礎(chǔ)——ClassLoader 源碼解析

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

Android 和 Java 平臺(tái)的類加載平臺(tái)區(qū)別較大患整,是我們基礎(chǔ)篇的重點(diǎn)舵稠,我們將從三個(gè)方面來講解 ClassLoader。

Java 中的 ClassLoader 回顧

Java平臺(tái)的ClassLoader

之前的文章中,我們已經(jīng)看過這張圖了贩疙,那篇文章中也簡單的講解了類的加載流程惭笑,加載流程兩個(gè)平臺(tái)差不多坯约,如何大家還不太熟悉可以去上面給出的虛擬機(jī)文章中再復(fù)習(xí)一下。

Android 中的 ClassLoader 詳解

Android 中的 ClassLoader 種類

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

  • BootClassLoader
  • PathClassLoader
  • DexClassLoader
  • BaseDexClassLoader

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

PathClassLoader 作用和 Java 中的 App ClassLoader 作用有點(diǎn)類似烘嘱,用來加載已經(jīng)安裝到系統(tǒng)中的 APK 文件中的 Class 文件昆禽。

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

BaseDexClassLoader 是一個(gè)父類醉鳖,DexClassLoader 和 PathClassLoader 都是它的子類。

一個(gè) App 至少需要 BootClassLoader 和 PathClassLoader 才能運(yùn)行遗契。為了證明這一點(diǎn)辐棒,我們寫一個(gè)簡單的頁面,在 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());
            }
        }

最后我們發(fā)現(xiàn)輸出dalvik.system.PathClassLoaderjava.lang.BootClassLoader牍蜂。當(dāng)然不同機(jī)子可能輸出的結(jié)果不同漾根,但至少會(huì)有這兩個(gè) ClassLoader。BootClassLoader 負(fù)責(zé)加載 framework 字節(jié)碼文件鲫竞,所以每個(gè)應(yīng)用都是需要的辐怕,而 PathClassLoader 用來加載已安裝 Apk 的字節(jié)碼文件,這些東西都是一個(gè)應(yīng)用啟動(dòng)的必要東西从绘。

Android 中 ClassLoader 特點(diǎn)及作用

Android 中的 ClassLoader 最大的特點(diǎn)就是雙親代理模型寄疏。雙親代理模型主要分三個(gè)過程:在加載字節(jié)碼的時(shí)候,會(huì)詢問當(dāng)前 ClassLoader 是否已經(jīng)加載過僵井,如果加載過則直接返回陕截,不再重復(fù)加載,如果沒有的話批什,會(huì)查詢 parent 是否加載過农曲,如果加載過,就直接返回 parent 加載的字節(jié)碼文件驻债。如果整個(gè)繼承線路上的 ClassLoader 都沒有加載乳规,執(zhí)行類才會(huì)由當(dāng)前 ClassLoader 類進(jìn)行真正加載。

這樣做的好處是合呐,如果一個(gè)類被位于樹中任意 ClassLoader 節(jié)點(diǎn)加載過暮的,那么以后整個(gè)系統(tǒng)生命周期中,這個(gè)類都將不會(huì)被加載淌实,大大提高了加載類的效率冻辩。由于這樣的特點(diǎn)猖腕,就給我們 ClassLoader 帶來了兩個(gè)作用。

第一個(gè)作用就是類加載的共享功能微猖。當(dāng)一個(gè) framework 層中的類被頂層 ClassLoader 加載過谈息,那么這個(gè)類就會(huì)被緩存在內(nèi)存里,以后任何需要用到底地方都不會(huì)重新加載了凛剥。

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

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

Android ClassLoader 源碼講解

我們下面就來通過源碼來看看 Android ClassLoader 到底是如何實(shí)現(xiàn)雙親代理模式的炊昆。

首先我們進(jìn)入 ClassLoader.java 這個(gè)類桨吊,查找它最核心的方法 loadClass() 看看它是怎么實(shí)現(xiàn)的

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 1.查看 class 是否已經(jīng)被加載過
            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.如果類沒有被加載過凤巨,那么就通過當(dāng)前 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;
    }

我在代碼中注釋已經(jīng)比較清楚了视乐,源碼中首先會(huì)判斷當(dāng)前的 ClassLoader 有沒有加載過這個(gè)類,如果沒有加載過敢茁,再會(huì)看看 parent ClassLoader 有沒有加載過佑淀,如果整個(gè)繼承線路走過后 class 依然為 null,則再回到當(dāng)前 ClassLoader 通過 findClass() 方法來加載 class彰檬。

好伸刃,現(xiàn)在讓我們繼續(xù)跟蹤 findClass()方法,進(jìn)去后發(fā)現(xiàn)這個(gè)方法是個(gè)空實(shí)現(xiàn)逢倍,說明真正的實(shí)現(xiàn)代碼都在 ClassLoader 的子類中實(shí)現(xiàn)捧颅,我們在 Android Studio 中,查找類似 PathClassLoader 這樣的類是無法看到代碼的较雕,所以我們可以通過源碼網(wǎng)站 AndroidXRef 或者其他觀看源碼的方式來查看下 Android 幾個(gè) ClassLoader 的具體實(shí)現(xiàn)隘道。

打開 DexClassLoader發(fā)現(xiàn)很簡單,類中只有一個(gè)構(gòu)造方法郎笆,繼承自 BaseDexClassLoader,下面我們來看看這個(gè)構(gòu)造方法忘晤。

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

參數(shù)dexpath指定我們要加載的 dex 文件路徑宛蚓,optimizedDirectory指定該 dex 文件要被拷貝到哪個(gè)路徑中,一般是應(yīng)用程序內(nèi)部路徑设塔。

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 文件,可以用來加載一些并沒有安裝到系統(tǒng)應(yīng)用中的類。所以痕钢,DexClassLoader 是動(dòng)態(tài)加載的核心图柏。

下面我們再來看看 PathClassLoader 是如何實(shí)現(xiàn)的,它同樣也是繼承于 BaseDexClassLoader任连,并且也重寫了構(gòu)造方法蚤吹。

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

我們可以看到,它和 DexClassLoader 的區(qū)別就在于少了一個(gè) optimizedDirectory 的參數(shù)随抠,所以 PathClassLoader 沒有辦法加載沒有安裝到系統(tǒng)中的應(yīng)用的類裁着。

我們發(fā)現(xiàn),這兩個(gè) ClassLoader 并沒有什么具體實(shí)現(xiàn)拱她,真正的實(shí)現(xiàn)都是在他們的父類 BaseDexClassLoader中二驰,所以我們下面看一下它的實(shí)現(xiàn)。

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;
    }
}

我們通過構(gòu)造方法可以觀察到秉沼,如果 optimizedDirectory 為空桶雀,那么代表這是 PathClassLoader,不為空則是 DexClassLoader唬复,findClass()方法雖然我們終于看到了實(shí)現(xiàn)矗积,但發(fā)現(xiàn)真正的實(shí)現(xiàn)還沒有在這里,而是在 DexPathList對象的findClass()方法中盅抚,不要?dú)怵H漠魏,結(jié)果就在前方,我們繼續(xù)跟進(jìn)妄均!

DexPathList這個(gè)類代碼比較多柱锹,我們來從它的成員變量中開始,挑重點(diǎn)看丰包。

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;
                }
            }
        }
    }
}

我們關(guān)注幾個(gè)點(diǎn)禁熏,一個(gè)是 DEX_SUFFIX 這個(gè)成員變量,代表 dex 文件后綴邑彪,方便后面的一些文件處理判斷使用瞧毙。 definingContext 就是在初始化的時(shí)候傳進(jìn)來的 ClassLoader,dexElements DexPathList 中一個(gè)靜態(tài)內(nèi)部類對象數(shù)組寄症,在構(gòu)造方法中初始化宙彪,這個(gè)對象數(shù)組是 findClass() 的關(guān)鍵參數(shù),通過遍歷獲取 Elements 中的 DexFile 對象有巧,調(diào)用 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 對象數(shù)組存儲(chǔ)的具體靜態(tài)內(nèi)部類篮迎,該類我只是簡單列舉下它的成員變量男图。dexElements 在 DexPathList 的構(gòu)造方法中初始化示姿,我們來細(xì)致的看下 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);
                    }
                }
            }
        }                                     
}

這里我省略掉了一些代碼栈戳,只看重點(diǎn)。其中注釋中第一個(gè)和第二個(gè) if 語句中的代碼的作用是如果路徑是文件夾的話难裆,就繼續(xù)向下遞歸子檀,第三個(gè)判斷是否是文件,如果是差牛,進(jìn)入第四個(gè)命锄,判斷文件是否是以 .dex 為后綴的,如果是的話標(biāo)明這個(gè)文件就是我們需要加載的 dex 文件偏化,通過 loadDexFile() 方法來加載 DexFile 對象脐恩。如果是文件,并且是個(gè)壓縮文件的話侦讨,就會(huì)進(jìn)入第五個(gè) if 語句中驶冒,同樣會(huì)通過 loadDexFile() 來進(jìn)行 DexFile 加載。下面來看一下 loadDexFile() 方法實(shí)現(xiàn)韵卤。

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 文件,那么直接創(chuàng)建 DexFile 對象即可沈条,如果不為空需忿,則調(diào)用 loadDex() 方法,將它解壓然后獲取內(nèi)部真正的 DexFile蜡歹。所以 makeElements() 就是通過文件獲取 dex 文件屋厘,轉(zhuǎn)化為 Elements 對象數(shù)組,然后給findClass() 方法使用月而。

loadClassBinaryName()方法再往下走就是 native 方法了汗洒,我們就無法繼續(xù)看了,大概可以想像這個(gè) native 方法就是通過 C父款、C++去查找 dex 指定 name 相關(guān)的東西溢谤,然后將它拼成 class 字節(jié)碼,最后返回給我們憨攒。

整體的源碼我們大概就看過了世杀,實(shí)際上不是很復(fù)雜,只是嵌套很多肝集,真正復(fù)雜的地方都在 native 中了玫坛,所以我們看源碼一定要耐心細(xì)心,不能懼怕包晰,看不懂就多看幾遍湿镀,學(xué)習(xí)一下他們的編程思路和設(shè)計(jì)思想,對我們能力提高有極大幫助伐憾。

Android 中的動(dòng)態(tài)加載比 Java 程序復(fù)雜在哪里

Android 中的動(dòng)態(tài)加載在我們之前源碼分析之后勉痴,感覺看起來不是很復(fù)雜,只要利用好幾個(gè) ClassLoader 树肃,整體的思路還是比較清晰的蒸矛,但在實(shí)際設(shè)計(jì)的時(shí)候遠(yuǎn)遠(yuǎn)沒有那么簡單,主要是因?yàn)?Android 有他的復(fù)雜性:

  • 有許多組件類胸嘴,比如四大組件雏掠,都是需要注冊才能使用的。需要在 AndoridManifest 注冊才能使用劣像。
  • 資源的動(dòng)態(tài)加載非常復(fù)雜乡话。Android 的資源很特殊,都是通過 id 注冊的耳奕,通過 id 從 Resource 實(shí)例中獲取對應(yīng)的資源绑青,如果是動(dòng)態(tài)加載的新類,資源 id 就會(huì)找不到屋群,總而言之就是資源也是需要?jiǎng)討B(tài)注冊的闸婴。
  • Android 每個(gè)版本對于類和資源加載的方式都是不同的,適配也是一個(gè)極為頭疼的問題芍躏。

以上難點(diǎn)總結(jié)起來可以用一句話概括:「Android 程序運(yùn)行需要一個(gè)上下文環(huán)境」邪乍。上下文環(huán)境可以給組件提供需要的功能,比如主題对竣、資源庇楞、查詢組件等。那么我們?nèi)绾谓o動(dòng)態(tài)加載的組件和類提供上下文環(huán)境呢柏肪,其實(shí)這就是第三方動(dòng)態(tài)加載庫主要解決的問題姐刁,也是非常復(fù)雜的,像 Tinker 和 Atlas 這些比較成熟的動(dòng)態(tài)加載方案都是以解決這些問題作為核心而設(shè)計(jì)的烦味,我們個(gè)人要解決可能比較困難聂使,但我們可以通過使用和閱讀源碼,來學(xué)習(xí)他們的實(shí)現(xiàn)原理谬俄,大致了解即可柏靶。


下一篇文章我們將利用我們學(xué)到的 ClassLoader 相關(guān)知識(shí),自己嘗試寫一個(gè)簡單的插件加載 demo 和插件管理器溃论。

本文部分內(nèi)容參考于慕課網(wǎng)實(shí)戰(zhàn)課程「Android 應(yīng)用發(fā)展趨勢必備武器 熱修復(fù)與插件化」屎蜓,有興趣的朋友可以付費(fèi)學(xué)習(xí)。
插件化實(shí)戰(zhàn)課程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钥勋,一起剝皮案震驚了整個(gè)濱河市炬转,隨后出現(xiàn)的幾起案子辆苔,更是在濱河造成了極大的恐慌,老刑警劉巖扼劈,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驻啤,死亡現(xiàn)場離奇詭異,居然都是意外死亡荐吵,警方通過查閱死者的電腦和手機(jī)骑冗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來先煎,“玉大人贼涩,你說我怎么就攤上這事∈硇” “怎么了遥倦?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長良风。 經(jīng)常有香客問我谊迄,道長,這世上最難降的妖魔是什么烟央? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任统诺,我火速辦了婚禮,結(jié)果婚禮上疑俭,老公的妹妹穿的比我還像新娘粮呢。我一直安慰自己,他們只是感情好钞艇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布啄寡。 她就那樣靜靜地躺著,像睡著了一般哩照。 火紅的嫁衣襯著肌膚如雪挺物。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天飘弧,我揣著相機(jī)與錄音识藤,去河邊找鬼。 笑死次伶,一個(gè)胖子當(dāng)著我的面吹牛痴昧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冠王,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼赶撰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起豪娜,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤餐胀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后瘤载,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骂澄,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年惕虑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片磨镶。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溃蔫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出琳猫,到底是詐尸還是另有隱情伟叛,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布脐嫂,位于F島的核電站统刮,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏账千。R本人自食惡果不足惜侥蒙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匀奏。 院中可真熱鬧鞭衩,春花似錦、人聲如沸娃善。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聚磺。三九已至坯台,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘫寝,已是汗流浹背蜒蕾。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矢沿,地道東北人滥搭。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像捣鲸,于是被迫代替她去往敵國和親瑟匆。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容