之前已經對我們學習插件化原理需要的預備知識進行了比較詳細的講解了,從這篇文章開始,我們將具體介紹插件化原理,同時會根據原理寫一個比較簡單的插件化管理器。
插件化主要用到的技術知識有:
- Android ClassLoader 加載 class 文件原理,這也是插件化最重要的技術點,我們在上篇文章中講解的也比較詳細了,插件化框架都會通過自定義 ClassLoader 來加載插件中的 class 文件。
- Java 反射原理,這是制作插件化框架中最基礎和最核心的知識點了。
- Android 資源加載原理,即 Android 如何加載資源文件,主要通過 Resource 類和 AssetManager 類等來完成資源的加載。
- 四大組件加載原理。了解四大組件的加載流程,以及它們是如何通過 ActivityManagerService 完成與系統的通信。
這四點是最基本要了解的點,你還需要了解像 Gradle 打包原理,Android Framework 層以及一些包的管理——PackageManager 的原理,所以如果想制作一個插件化框架,實際上是非常復雜的,要對 Android 系統非常熟悉,這里我們只講解基本原理。
Manifest 處理
清單文件在 App 中非常重要,它記錄了你的應用中有哪些組件。
這是市面上大部分的框架對清單文件的處理方式。首先,無論是宿主 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 應用發展趨勢必備武器 熱修復與插件化」,有興趣的朋友可以付費學習。
插件化實戰課程