ZeusPlugin 是掌閱開源的一個插件補丁框架,最近在使用的過程中也一邊在研究原理,在此記錄一下。
項目GitHub地址:ZeusPlugin
Android插件化主要需要解決兩個問題:代碼加載和資源加載。
代碼加載
系統類加載機制
雙親委託模型
Java的JVM虛擬機運行程序時,通過ClassLoader把需要的Class從jar文件加載到內存中。Android的Dalvik/ART虛擬機與標準Java的JVM虛擬機不一樣,ClassLoader加載的是dex文件,但是兩者工作機制類似,都實現了雙親委託模型(Parent-Delegation Model)。
ClassLoader的構造方法如下:
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
創建一個ClassLoader實例的時候,需要使用一個現有的ClassLoader實例作爲新創建的實例的Parent(啓動類加載器bootstrap class loader除外)。這樣一來,一個Android應用裏所有的ClassLoader實例都會被一棵樹關聯起來。在加載類時,ClassLoader會優先委託parent去查詢。
JVM中ClassLoader通過defineClass方法加載jar裏面的Class,而Android中這個方法被棄用了,取而代之的是loadClass方法:
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className); //在當前ClassLoader已加載過的類中查找
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false); //在parent中查找
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className); //在自己中查找
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
可以看到,類的加載會首先查詢緩存(已加載到內存的部分);然後查詢Parent是否已加載過該類,如果已經加載過就直接返回Parent加載的類;最後纔會在當前ClassLoader查詢並加載。因此如果一個類被位於樹根的ClassLoader加載過,這個類就不會被重新加載。
BaseDexClassLoader
Android的 Dalvik/ART 虛擬機是通過 dex 或者 包含 dex 的jar、apk 文件來加載,主要邏輯都在 ClassLoader 的子類 BaseDexClassLoader 中。實際開發過程中,一般使用它的子類DexClassLoader、PathClassLoader這些類加載器來加載類。
在 Android 中,App 安裝到手機後,apk 裏面的 class.dex 中的 class 均是通過 PathClassLoader 來加載的。在項目中驗證一下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, classLoader.toString());
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG, classLoader.toString());
}
}
}
輸出結果:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.test.classloadertest-2/base.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
java.lang.BootClassLoader@3930d8b6
可以看見有2個Classloader實例,一個是BootClassLoader(系統啓動的時候創建的),另一個是PathClassLoader(應用啓動時創建的,用於加載apk裏面的類)。
PathClassLoader、DexClassLoader
PathClassLoader 和 DexClassLoader 都是BaseDexClassLoader的子類,兩者的構造函數:
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
- dexPath : 包含 dex 的 jar 文件或 apk 文件的路徑集
- libraryPath : 包含 C/C++ 庫的路徑集
- dexPath : 包含 class.dex 的 apk、jar 文件路徑
- optimizedDirectory : 用來緩存優化的 dex 文件的路徑,即從 apk 或 jar 文件中提取出來的 dex 文件
可以看到區別在於 optimizedDirectory 這個參數,PathClassLoader 中該參數爲null,只能加載內部的dex,這些大都是存在系統中已經安裝過的apk裏面的。
DexClassLoader 中該參數不能爲空,它可以加載外部的dex(包括jar/apk,以及sd卡中未安裝的apk),這個dex會被複制到內部路徑的optimizedDirectory。因此一般需要動態加載時使用 DexClassLoader 這個類。
ZeusPlugin實現策略
ZuesPlugin 的實現中爲插件及補丁分別添加相應的ClassLoader,並仿照系統類加載的雙親委託模型,設置它們的父子關係。其ClassLoader設計如下:
通過反射修改系統的ClassLoader爲ZeusClassLoader,其內包含多個ZeusPluginClassLoader。
每一個插件對應一個ZeusPluginClassLoader,當移除插件時則刪除一個ZeusPluginClassLoader,加載一個插件則添加一個ZeusPluginClassLoader。
ZeusClassLoader的parent爲原始APK的ClassLoader,而原始APK的ClassLoader的parent爲ZeusHotfixClassLoader, ZeusHotfixClassLoader的parent爲系統rom的ClassLoader。
- ZeusClassLoader.java
ZeusClassLoader是一個容器類,其內維護了一個ZeusPluginClassLoader的列表,可以動態添加或刪除ZeusPluginClassLoader。加載類時會依次到各個ZeusPluginClassLoader查找。它的parent是原APK的ClassLoader。
ZeusClassLoader重寫了loadClass方法如下:
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
try {
//先查找parent classLoader,這裏實際就是系統幫我們創建的classLoader,目標對應爲宿主apk
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException ignored) {
}
if (clazz != null) {
return clazz;
}
//挨個的到插件裏進行查找
if (mClassLoader != null) {
for (ZeusPluginClassLoader classLoader : mClassLoader) {
if (classLoader == null) continue;
try {
//這裏只查找插件它自己的apk,不需要查parent,避免多次無用查詢,提高性能
clazz = classLoader.loadClassByself(className);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException ignored) {
}
}
}
throw new ClassNotFoundException(className + " in loader " + this);
}
- ZeusPluginClassLoader.java
在ZeusClassLoader中已經查找過parent,因此插件類加載通過loadClassByself方法,直接在本身查找。
public Class<?> loadClassByself(String className) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
- ZeusHotfixClassLoader.java
ZeusHotfixClassLoader是宿主APK ClassLoader的parent,因此加載類時會先加載補丁中的類。它的loadClass方法多了查找宿主APK的部分,如果補丁中未找到所需的類就會使用原APK中的類。它的parent是系統rom的ClassLoader。
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//先查找補丁自己已經加載過的有沒有
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
//查查parent中有沒有,也就是android系統中的
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException ignored) {
}
if (clazz == null) {
try {
//查查自己有沒有,就是補丁中有沒有
clazz = findClass(className);
} catch (ClassNotFoundException ignored) {
}
}
}
//查查child中有沒有,child是設置進來的,實際就是宿主apk中有沒有
if (clazz == null && mChild != null) {
try {
if (findLoadedClassMethod != null) {
clazz = (Class<?>) findLoadedClassMethod.invoke(mChild, className);
}
if (clazz != null) return clazz;
if (findClassMethod != null) {
clazz = (Class<?>) findClassMethod.invoke(mChild, className);
return clazz;
}
} catch (Exception ignored) {
}
}
if (clazz == null) {
throw new ClassNotFoundException(className + " in loader " + this);
}
return clazz;
}
- PluginManager.java
PluginManager是插件管理類,管理插件的初始化、安裝、卸載、加載等。在程序初始化時,PluginManager的init方法會執行一些初始安裝加載的操作,其中會調用loadInstalledPlugins方法,這個方法中會設置插件、補丁ClassLoader與原APK的ClassLoader的父子關係。
//非完整代碼,刪去了與ClassLoader無關的部分
private static void loadInstalledPlugins() {
synchronized (mLoadLock) {
//...some code...
//獲取classloader設置classloader
ZeusClassLoader classLoader = null;
for (String pluginId : installedPluginMaps.keySet()) {
if (PluginUtil.isPlugin(pluginId)) {
if (classLoader == null) {
classLoader = new ZeusClassLoader(mBaseContext.getClassLoader()); //parent爲原APK的ClassLoader
}
classLoader.addAPKPath(pluginId, PluginUtil.getAPKPath(pluginId),
PluginUtil.getLibFileInside(pluginId),
PluginUtil.getInstalledPathInfo(pluginId));
}
//熱修復補丁,補丁一般只針對某個版本
if (PluginUtil.isHotFix(pluginId)) {
try {
loadHotfixPluginClassLoader(pluginId); //該方法中會重設ZuesHotfixClassLoader的父子關係
} catch (Exception e) {
e.printStackTrace();
}
}
}
//設置原始APK所使用的ClassLoader
if (classLoader != null) {
PluginUtil.setField(mPackageInfo, "mClassLoader", classLoader);
Thread.currentThread().setContextClassLoader(classLoader);
mNowClassLoader = classLoader;
}
//...some code...
}
}
最終系統、原程序、插件、補丁的ClassLoader之間的父子關係如下圖所示:
*以上圖片來自ZeusPlugin交流羣中的PPT.
資源加載
系統資源加載
Android 中通過 ContextImpl 類的兩個成員函數 getResources 和 getAssets 來訪問資源。
getResources 返回的是 ContextImpl 的成員變量 mResources,這個Resources對象可以通過資源ID來訪問那些被編譯過的應用程序資源。
Resources 的成員變量 mAssets 是一個 AssetManager 對象,getAssets 方法通過ContextImpl 的成員變量 mResources 的成員函數 getAssets 來獲得該 AssetManager 對象。
AssetManager 可以通過文件名來訪問那些被編譯過或者沒有被編譯過的應用程序資源文件,事實上,Resources類也是通過AssetManager類來訪問那些被編譯過的應用程序資源文件的,不過在訪問之前,它會先根據資源ID查找得到對應的資源文件名。
Android應用程序除了要訪問自己的資源之外,還需要訪問系統的資源。應用程序進程通過一個單獨的 Resources 對象和一個單獨的 AssetManager 對象來管理系統資源,這兩個對象分別是 Resources 類的成員函數 mSystem 和 AssetManager 類的成員函數 sSystem。
ContextImpl、Resourses和AssetManager的關係如圖:
* 圖片來自這裏
AssetManager 中通過一個隱藏方法 addAssetPath 來添加資源:
/** Add an additional set of assets to the asset manager.
* This can be either a directory or ZIP file.
* Not for use by applications. Returns the cookie of the added asset,
* or 0 on failure.
*@{hide}
*/
public native final int addAssetPath(String path);
該方法是一個 JNI 方法,具體實現由 C++ 層的相應方法完成。C++ 層維護有一個記錄asset_path 的Vector,調用 addAssetPath後會將參數 path 所描述的 apk 文件路徑添加到 Vector 中。
系統資源文件 framework-res.apk 也是通過這個方法添加到 AssetManager 中,所以Resources中的 AssetManager 既可以訪問系統也可以訪問 apk 資源。
ZeusPlugin 實現策略
類似於系統的資源加載過程,想要訪問插件、補丁的資源,只需要將它們的apk路徑也通過 addAssetPath 添加到 AssetManager 中就行了。創建一個新的 AssetManager 對象,通過反射調用 addAssetPath 方法,然後以這個 AssetManager 爲參數創建 Resources 對象,並反射修改系統使用的 Resources。
這一系列步驟在 PluginManager 中的 reloadInstalledPluginResources 方法中完成:
//非完整代碼
private static void reloadInstalledPluginResources() {
try {
//創建AssetManager,反射調用addAssetPath
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());
if (mLoadedPluginList != null && mLoadedPluginList.size() != 0) {
for (String id : mLoadedPluginList.keySet()) {
if (!PluginUtil.isHotfixWithoutResFile(id)) {
addAssetPath.invoke(assetManager, PluginUtil.getAPKPath(id));
}
}
}
//創建Resources
PluginResources newResources = new PluginResources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration());
//替換Resources
PluginUtil.setField(mBaseContext, "mResources", newResources);
PluginUtil.setField(mPackageInfo, "mResources", newResources);
//...some code...
} catch (Throwable e) {
e.printStackTrace();
}
}
這個方法在程序初始化和加載新插件的時候都會調用,一旦插件發生變化就會使用一個新的 Resources,避免原來 Resources 的緩存造成影響。
參考
https://github.com/iReaderAndroid/ZeusPlugin/wiki
https://segmentfault.com/a/1190000004062880
http://www.jianshu.com/p/22230ed1b6e2
http://jaeger.itscoder.com/android/2016/08/27/android-classloader.html
http://blog.csdn.net/luoshengyang/article/details/8791064