Android動態(tài)加載之ClassLoader —熱修復(fù)、插件化

Android平臺上虛擬機(jī)運(yùn)行的是Dex字節(jié)碼,一種對class文件優(yōu)化的產(chǎn)物,傳統(tǒng)Class文件是一個(gè)Java源碼文件會生成一個(gè).class文件,而Android是把所有Class文件進(jìn)行合并,優(yōu)化,然后生成一個(gè)最終的class.dex,目的是把不同class文件重復(fù)的東西只需保留一份,如果我們的Android應(yīng)用不進(jìn)行分dex處理,最后一個(gè)應(yīng)用的apk只會有一個(gè)dex文件。

Android平臺的ClassLoader

image.png

我們主要用到了兩種類加載器 PathClassLoaderDexClassLoader 。

他們的區(qū)別是:
PathClassLoader:是 Android 應(yīng)用中默認(rèn)的類加載器,只能加載已經(jīng)安裝到 Android 系統(tǒng)中的apk文件(/data/app目錄下,解壓為 dex 后優(yōu)化為 odex)。

DexClassLoader:可以加載路徑下的 dex/jar/apk/zip 文件,比 PathClassLoader 更靈活,是實(shí)現(xiàn)熱修復(fù)的關(guān)鍵。

首先說下雙親委托模式

雙親委托模式的特點(diǎn)
類加載器查找Class所采用的是雙親委托模式,所謂雙親委托模式就是首先判斷該Class是否已經(jīng)加載,如果沒有則不是自身去查找而是委托給父加載器進(jìn)行查找,這樣依次的進(jìn)行遞歸,直到委托到最頂層的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了該Class,就會直接返回,如果沒找到,則繼續(xù)依次向下查找,如果還沒找到則最后會交由自身去查找。
這樣講可能會有些抽象,來看下面的圖。


image.png

再說dex文件的加載和類的查找過程
Java層通過我們會通過創(chuàng)建一個(gè)DexClassLoader來加載我們的dex,下面就以此為切入點(diǎn)進(jìn)行

創(chuàng)建ClassLoader
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());

查看DexClassLoader的構(gòu)造方法。

public class DexClassLoader extends BaseDexClassLoader {
    // dexPath:是加載apk/dex/jar的路徑
    // optimizedDirectory:是優(yōu)化dex后得到的.odex文件的輸出路徑
    // libraryPath:是加載的時(shí)候需要用到的so庫
    // parent:給DexClassLoader指定父加載器
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以看到它調(diào)用的是父類的構(gòu)造函數(shù),所以直接來看BaseDexClassLoader的構(gòu)造函數(shù)。

 private final DexPathList pathList;

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

創(chuàng)建了一個(gè)DexPathList實(shí)例,下面來看看DexPathList的構(gòu)造函數(shù)。

private final Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
}

//它調(diào)用的是makeDexElements方法來創(chuàng)建一個(gè)Element數(shù)組來存放Element對象,

//每個(gè)Element對象包含一個(gè)DexFile對象。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        // 如果是一個(gè)dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ex) {
                System.logE("Unable to load dex file: " + file, ex);
            }
        // 如果是一個(gè)apk或者jar或者zip文件
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = file;

            try {
                // 1、調(diào)用loadDexFile加載dex文件,得到一個(gè)DexFile對象
                 //   loadDexFile通過c++層native方法去加載dex文件
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException suppressed) {
               
                suppressedExceptions.add(suppressed);
            }
        } else if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else {
            System.logW("Unknown file type for: " + file);
        }
        
        // 2、把DexFile對象封裝到Element對象中,然后將Element對象加入Element數(shù)組
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

dex文件的加載流程:我們會使用DexClassLoader去加載dex文件,DexClassLoader會將這個(gè)任務(wù)委派給DexPathList中的makeDexElements方法,在makeDexElements中調(diào)用了native層的 c++方法去真正的加載dex文件,然后返回DexFile的對象,通過這個(gè)對象構(gòu)建一個(gè)Element的對象,然后將這個(gè)Element添加到dexElements的數(shù)組中。

類的加載過程

當(dāng)類加載器收到加載類或資源的請求時(shí),通常都是先委托給父類加載器加載,也就是說只有當(dāng)父類加載器找不到指定類或資源時(shí),自身才會執(zhí)行實(shí)際的類加載過程,源碼如下

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    // 首先從已經(jīng)加載的類中查找
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            // 如果沒有加載過,先調(diào)用父加載器的 loadClass
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }
        if (clazz == null) {
            try {
                // 父加載器都沒有加載,則嘗試自己去加載
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}

一句話概括:ClassLoader 加載類時(shí),先查看自身是否已經(jīng)加載過該類,如果沒有加載過會首先讓父加載器去加載,如果父加載器無法加載該類時(shí)才會調(diào)用自身的 findClass 方法加載,該邏輯避免了類的重復(fù)加載。所以我們所要實(shí)現(xiàn)的就是把要替換的類可見性提前,這樣類加載器就會優(yōu)先找到修復(fù)過的類。

類的查找過程

//DexClassLoader間接調(diào)用父類findClass方法
//,findClass方法中調(diào)用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

//看DexPathList中的findClass方法,可以看到它是遍歷dexElements數(shù)組,
//到每個(gè)dex文件去尋找當(dāng)前需要的類,找到之后直接返回不往下找了
    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

類的查找過程:DexClassLoader通過findClass去查找一個(gè)類,同樣它也是委派給DexPathList的findClass去查找,在DexPathList的findClass中會去遍歷我們上面創(chuàng)建的dexElements數(shù)組,然后在每個(gè)dex中去查找相應(yīng)的類,找到之后就返回,不再向后查找。

熱修復(fù)過程

1、PathClassLoader 作為默認(rèn)的類加載器,也就是第一個(gè) DEX 文件是 PathClassLoader 自動加載的。
2、通過前面的分析來看,我們知道 PathClassLoader 里面的 DEX 文件是存放在一個(gè) Element 數(shù)組中,可以包含多個(gè) DEX 文件,所以我們只需要通過反射獲取 PathClassLoader 中的 DexPathList 中的Element數(shù)組(已加載了第一個(gè)dex包,由系統(tǒng)加載),將要替換的 DEX 文件放置到這個(gè)數(shù)組中去。
3、將兩個(gè)Element數(shù)組合并之后,再將其賦值給 PathClassLoader 的 Element 數(shù)組
Bugly熱更新是基于Tinker的 Bugly 熱更新原理圖如下:


image.png

image.png

如版本 1.0.0 上線后有bug 啟動1.0.0 版本app的時(shí)候 會把當(dāng)前對應(yīng)的TinkerId上報(bào)到平臺,
修改相關(guān)代碼 執(zhí)行第二步驟 打補(bǔ)丁包后 補(bǔ)丁包里面會有一個(gè)YAPATH.md文件,from 表示我發(fā)布的版本, to 表示我現(xiàn)在打補(bǔ)丁包,意思就是 這個(gè)1.0.0-patch這個(gè)補(bǔ)丁包 是針對于1.0.0-base 來修復(fù)的,
上傳補(bǔ)丁包到配置平臺后,平臺會自動解析YAPATH.md文件, 然后針對性的下發(fā)補(bǔ)丁。

這篇文章寫得很好

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容