Android插件化進(jìn)階——插件化原理和插件管理器(一)

之前已經(jīng)對(duì)我們學(xué)習(xí)插件化原理需要的預(yù)備知識(shí)進(jìn)行了比較詳細(xì)的講解了,從這篇文章開(kāi)始,我們將具體介紹插件化原理,同時(shí)會(huì)根據(jù)原理寫(xiě)一個(gè)比較簡(jiǎn)單的插件化管理器。

插件化主要用到的技術(shù)知識(shí)有:

  • Android ClassLoader 加載 class 文件原理,這也是插件化最重要的技術(shù)點(diǎn),我們?cè)谏掀恼轮兄v解的也比較詳細(xì)了,插件化框架都會(huì)通過(guò)自定義 ClassLoader 來(lái)加載插件中的 class 文件。
  • Java 反射原理,這是制作插件化框架中最基礎(chǔ)和最核心的知識(shí)點(diǎn)了。
  • Android 資源加載原理,即 Android 如何加載資源文件,主要通過(guò) Resource 類(lèi)和 AssetManager 類(lèi)等來(lái)完成資源的加載。
  • 四大組件加載原理。了解四大組件的加載流程,以及它們是如何通過(guò) ActivityManagerService 完成與系統(tǒng)的通信。

這四點(diǎn)是最基本要了解的點(diǎn),你還需要了解像 Gradle 打包原理,Android Framework 層以及一些包的管理——PackageManager 的原理,所以如果想制作一個(gè)插件化框架,實(shí)際上是非常復(fù)雜的,要對(duì) Android 系統(tǒng)非常熟悉,這里我們只講解基本原理。

Manifest 處理

清單文件在 App 中非常重要,它記錄了你的應(yīng)用中有哪些組件。

Manifest處理

這是市面上大部分的框架對(duì)清單文件的處理方式。首先,無(wú)論是宿主 app 還是 aar 還是 Bundle,都是有自己的清單文件的,那么我們平時(shí)使用的時(shí)候,當(dāng)我們依賴(lài) library 或者 aar 的時(shí)候,就會(huì)有多個(gè)清單文件,這個(gè)時(shí)候,Gradle 在構(gòu)建 app 產(chǎn)物的時(shí)候,會(huì)將 aar 的 manifest 文件 merge 到 app module 中的清單文件中。

基于這個(gè)原理,插件化框架會(huì)修改整個(gè)打包流程,在輸出 apk 的 manifest 文件的時(shí)候,會(huì)將所有插件的清單文件都合并到宿主清單文件中,這樣的話(huà),宿主 manifest 就記錄了所有插件和 aar 文件中所有清單的內(nèi)容,這樣就可以保證我們調(diào)用各個(gè)插件組件的時(shí)候不會(huì)報(bào)錯(cuò)。

光是清單文件的處理,就比較復(fù)雜了。你不僅要了解 Gradle 的打包原理,甚至還需要去修改流程。這樣才能打到合并清單文件的要求。

我們來(lái)總結(jié)下插件化框架對(duì)于 manifest 的處理,主要工作主要有兩個(gè):

  • 文件的合并,實(shí)際上就是一個(gè) IO 操作,將所有的清單文件合并到一個(gè)總的 manifest 文件中。
  • 修改構(gòu)建流程,在構(gòu)建時(shí)將所有插件的清單文件合并到宿主的 manifest 文件中。大家有興趣的話(huà)可以深入了解下如何修改編譯流程從而完成清單文件的合并。

插件類(lèi)加載

插件類(lèi)加載

每個(gè)插件實(shí)際上都是一個(gè) APK,每個(gè) APK 有自己的 Dex 文件,所有的 class 字節(jié)碼就都存儲(chǔ)在了這些 dex 文件中的。市面上絕大多數(shù)的插件化框架都是根據(jù)上圖的這樣的方式來(lái)加載插件中的類(lèi)的。

在加載之前,他首先會(huì)區(qū)分宿主 apk 和 插件 apk,這樣區(qū)分的好處是,因?yàn)樗拗?apk 已經(jīng)安裝到了系統(tǒng)中了,所以系統(tǒng)會(huì)給宿主 apk 創(chuàng)建 ClassLoader ,而無(wú)需手動(dòng)去創(chuàng)建了。所以宿主 apk 的 ClassLoader 使用 PathClassLoader 就完全夠用了,PathClassLoader 可以加載已安裝 apk 中的類(lèi),這個(gè)我們?cè)谥暗奈恼轮幸呀?jīng)分析過(guò)了。

對(duì)于插件 apk 來(lái)說(shuō),因?yàn)闆](méi)有安裝到我們系統(tǒng)中,所以插件 apk 本身是沒(méi)有 ClassLoader 的,系統(tǒng)也不會(huì)幫我們創(chuàng)建,需要我們自己手動(dòng)創(chuàng)建 apk,插件化框架會(huì)給每個(gè)插件創(chuàng)建對(duì)應(yīng)的 ClassLoader,在加載插件中 apk 文件的時(shí)候,就使用我們創(chuàng)建的 ClassLoader 來(lái)加載插件 apk 中的類(lèi)。

根據(jù)這個(gè)思路,我們就會(huì)引出兩個(gè)問(wèn)題:

  • 如何自定義 ClassLoader 加載類(lèi)文件
  • 如何調(diào)用插件 apk 文件中的類(lèi)

下面我們就簡(jiǎn)單實(shí)現(xiàn)下上面兩個(gè)問(wèn)題的代碼,通過(guò)簡(jiǎn)單的模擬來(lái)理解原理。

首先我們新建一個(gè)項(xiàng)目,并且在項(xiàng)目下再新建一個(gè) Project 作為插件模塊。


項(xiàng)目結(jié)構(gòu)

這里的app代表宿主模塊,app.bundle代表某個(gè)插件模塊,名字大家不要過(guò)多糾結(jié),這里只是為了遵循 Small 框架而起的插件名。app.bundle中有一個(gè)簡(jiǎn)單的類(lèi),靜態(tài)方法輸出一段 Log:

public class BundleUtil {
    public static void printLog(){
        Log.e("Bundle","I am a class in the Bundle");
    }
}

現(xiàn)在我們的宿主模塊并沒(méi)有在build.gradle中 compile 這個(gè)模塊,我們要手動(dòng)在宿主 apk 中加載并調(diào)用這個(gè)類(lèi)?,F(xiàn)在我們?cè)谒拗髂K的 MainActivity 中的 onCreate() 方法中實(shí)現(xiàn)加載邏輯。

protect void onCreate(Bundle savedInstanceState){
    //省略一些代碼
    ...
    String apkPath = getExternalCacheDir().getAbsolutePath()+"/bundle-debug.apk";
    loadApk(apkPath);
}

private void loadApk(String apkPath) {
    //應(yīng)用內(nèi)部目錄,MODE_PRIVATE 代表只有自己應(yīng)用可以訪(fǎng)問(wèn)這個(gè)路徑。
    File optDir = getDir("opt", MODE_PRIVATE);
    //初始化 classLoader,通過(guò) DexClassLoader 來(lái)加載指定目錄下的插件中的類(lèi)
    DexClassLoader classLoader = new DexClassLoader(apkPath,
            optDir.getAbsolutePath(), null, this.getClassLoader());

    try {
        //獲取指定路徑插件的 class 字節(jié)碼文件
        Class cls = classLoader.loadClass("org.sojex.stockquotes.bundle.BundleUtil");
        if (cls != null) {
            Object instance = cls.newInstance();
            Method method = cls.getMethod("printLog");
            method.invoke(instance);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
 }

這里apkPath指的是插件 apk 存放的路徑,我們把app.bundle通過(guò)./gradlew assembleDebug指令打出 apk 包,并通過(guò)adb push命令推送到手機(jī)上的 apkPath 對(duì)應(yīng)的目錄中。

loadApk()方法是我們的核心方法,我們傳入一個(gè)apkPath參數(shù),指定插件 apk 存放的路徑,再通過(guò)Context.getDir獲取一個(gè)應(yīng)用內(nèi)部路徑,使用這兩個(gè)參數(shù),可以新建一個(gè)DexClassLoader對(duì)象,我們之前講過(guò),DexClassLoader可以加載沒(méi)有安裝的 apk 文件中的類(lèi),通過(guò)它的loadClass方法,獲取到BundleUtil的字節(jié)碼文件。最后通過(guò)反射,即可調(diào)用到插件 apk 類(lèi)中的 printLog()方法。我們運(yùn)行宿主 apk ,發(fā)現(xiàn)結(jié)果成功的打印了出來(lái)。

我們下面自定義一個(gè) ClassLoader。

public class CustomClassLoader extends DexClassLoader {
    public CustomClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);
    }

    /**
     * 定義 ClassLoder 要以何種策略加載 class 文件
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData != null) {
            return defineClass(name, classData, 0, classData.length);
        } else {
            throw new ClassNotFoundException();
        }
    }

    private byte[] getClassData(String name) {
        try {
            InputStream inputStream = new FileInputStream(name);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = -1;
            while ((bytesNumRead = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

自定義一個(gè) ClassLoader 最核心的就是重寫(xiě)findClass方法,方法里要定義我們要以何種策略加載 class 文件?,F(xiàn)在我們沒(méi)有什么特殊的策略,

這里就是通過(guò)指定類(lèi)的路徑加載字節(jié)碼,最后通過(guò)獲取到的字節(jié)碼轉(zhuǎn)化為 class 對(duì)象,getClassData方法就是一個(gè)簡(jiǎn)單的文件讀取。這里只是一個(gè)模擬如何定義 ClassLoader。通過(guò)自定義 ClassLoader,表明插件化框架可以為每一個(gè)插件維護(hù)一個(gè) ClassLoader,在加載普通類(lèi)的時(shí)候就會(huì)繞過(guò) Android 系統(tǒng)的加載機(jī)制,即使沒(méi)有安裝這些插件 apk,我們依然能加載其中的類(lèi)。

因?yàn)?Android 系統(tǒng)在加載 apk 的時(shí)候會(huì)創(chuàng)建一個(gè) PathClassLoader,而插件 apk 的加載繞過(guò)了 Android 系統(tǒng),所以我們就要手動(dòng)的為每一個(gè)插件 apk 都要?jiǎng)?chuàng)建一個(gè) ClassLoader。不僅如此,如果宿主和各插件 apk 中有同名類(lèi),如果不為每個(gè)插件創(chuàng)建 ClassLoader,那么如果該同名類(lèi)已經(jīng)被 ClassLoader 加載過(guò),其他的同名類(lèi)就無(wú)法再被加載了,而不同的 ClassLoader 的同名類(lèi)不會(huì)被判定為同一個(gè)類(lèi),插件中的同名類(lèi)在調(diào)用的時(shí)候依然會(huì)被加載。

當(dāng)然,真正的商業(yè)插件化框架不會(huì)這么簡(jiǎn)單,類(lèi)加載模塊不僅要完成類(lèi)的查找和加載,還要對(duì)插件的 ClassLoader 進(jìn)行管理,確保所有類(lèi)都能加載。

源碼地址

那么下一篇文章我們將寫(xiě)一個(gè)簡(jiǎn)單的插件管理器來(lái)模擬插件化框架的管理步驟。


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

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

相關(guān)閱讀更多精彩內(nèi)容

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