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

之前已經對我們學習插件化原理需要的預備知識進行了比較詳細的講解了,從這篇文章開始,我們將具體介紹插件化原理,同時會根據原理寫一個比較簡單的插件化管理器。

插件化主要用到的技術知識有:

  • Android ClassLoader 加載 class 文件原理,這也是插件化最重要的技術點,我們在上篇文章中講解的也比較詳細了,插件化框架都會通過自定義 ClassLoader 來加載插件中的 class 文件。
  • Java 反射原理,這是制作插件化框架中最基礎和最核心的知識點了。
  • Android 資源加載原理,即 Android 如何加載資源文件,主要通過 Resource 類和 AssetManager 類等來完成資源的加載。
  • 四大組件加載原理。了解四大組件的加載流程,以及它們是如何通過 ActivityManagerService 完成與系統的通信。

這四點是最基本要了解的點,你還需要了解像 Gradle 打包原理,Android Framework 層以及一些包的管理——PackageManager 的原理,所以如果想制作一個插件化框架,實際上是非常復雜的,要對 Android 系統非常熟悉,這里我們只講解基本原理。

Manifest 處理

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

Manifest處理

這是市面上大部分的框架對清單文件的處理方式。首先,無論是宿主 app 還是 aar 還是 Bundle,都是有自己的清單文件的,那么我們平時使用的時候,當我們依賴 library 或者 aar 的時候,就會有多個清單文件,這個時候,Gradle 在構建 app 產物的時候,會將 aar 的 manifest 文件 merge 到 app module 中的清單文件中。

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

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

我們來總結下插件化框架對于 manifest 的處理,主要工作主要有兩個:

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

插件類加載

插件類加載

每個插件實際上都是一個 APK,每個 APK 有自己的 Dex 文件,所有的 class 字節碼就都存儲在了這些 dex 文件中的。市面上絕大多數的插件化框架都是根據上圖的這樣的方式來加載插件中的類的。

在加載之前,他首先會區分宿主 apk 和 插件 apk,這樣區分的好處是,因為宿主 apk 已經安裝到了系統中了,所以系統會給宿主 apk 創建 ClassLoader ,而無需手動去創建了。所以宿主 apk 的 ClassLoader 使用 PathClassLoader 就完全夠用了,PathClassLoader 可以加載已安裝 apk 中的類,這個我們在之前的文章中已經分析過了

對于插件 apk 來說,因為沒有安裝到我們系統中,所以插件 apk 本身是沒有 ClassLoader 的,系統也不會幫我們創建,需要我們自己手動創建 apk,插件化框架會給每個插件創建對應的 ClassLoader,在加載插件中 apk 文件的時候,就使用我們創建的 ClassLoader 來加載插件 apk 中的類。

根據這個思路,我們就會引出兩個問題:

  • 如何自定義 ClassLoader 加載類文件
  • 如何調用插件 apk 文件中的類

下面我們就簡單實現下上面兩個問題的代碼,通過簡單的模擬來理解原理。

首先我們新建一個項目,并且在項目下再新建一個 Project 作為插件模塊。


項目結構

這里的app代表宿主模塊,app.bundle代表某個插件模塊,名字大家不要過多糾結,這里只是為了遵循 Small 框架而起的插件名。app.bundle中有一個簡單的類,靜態方法輸出一段 Log:

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

現在我們的宿主模塊并沒有在build.gradle中 compile 這個模塊,我們要手動在宿主 apk 中加載并調用這個類。現在我們在宿主模塊的 MainActivity 中的 onCreate() 方法中實現加載邏輯。

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

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

    try {
        //獲取指定路徑插件的 class 字節碼文件
        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通過./gradlew assembleDebug指令打出 apk 包,并通過adb push命令推送到手機上的 apkPath 對應的目錄中。

loadApk()方法是我們的核心方法,我們傳入一個apkPath參數,指定插件 apk 存放的路徑,再通過Context.getDir獲取一個應用內部路徑,使用這兩個參數,可以新建一個DexClassLoader對象,我們之前講過,DexClassLoader可以加載沒有安裝的 apk 文件中的類,通過它的loadClass方法,獲取到BundleUtil的字節碼文件。最后通過反射,即可調用到插件 apk 類中的 printLog()方法。我們運行宿主 apk ,發現結果成功的打印了出來。

我們下面自定義一個 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;
    }
}

自定義一個 ClassLoader 最核心的就是重寫findClass方法,方法里要定義我們要以何種策略加載 class 文件。現在我們沒有什么特殊的策略,

這里就是通過指定類的路徑加載字節碼,最后通過獲取到的字節碼轉化為 class 對象,getClassData方法就是一個簡單的文件讀取。這里只是一個模擬如何定義 ClassLoader。通過自定義 ClassLoader,表明插件化框架可以為每一個插件維護一個 ClassLoader,在加載普通類的時候就會繞過 Android 系統的加載機制,即使沒有安裝這些插件 apk,我們依然能加載其中的類。

因為 Android 系統在加載 apk 的時候會創建一個 PathClassLoader,而插件 apk 的加載繞過了 Android 系統,所以我們就要手動的為每一個插件 apk 都要創建一個 ClassLoader。不僅如此,如果宿主和各插件 apk 中有同名類,如果不為每個插件創建 ClassLoader,那么如果該同名類已經被 ClassLoader 加載過,其他的同名類就無法再被加載了,而不同的 ClassLoader 的同名類不會被判定為同一個類,插件中的同名類在調用的時候依然會被加載。

當然,真正的商業插件化框架不會這么簡單,類加載模塊不僅要完成類的查找和加載,還要對插件的 ClassLoader 進行管理,確保所有類都能加載。

源碼地址

那么下一篇文章我們將寫一個簡單的插件管理器來模擬插件化框架的管理步驟。


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

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

推薦閱讀更多精彩內容