自己動手實現 Android App 插件化
Android插件化目前國內已經有很多開源的工程了,不過如果不實際開發一遍,很難掌握的很好。
下面是自己從0開始,結合目前開源的項目和博客,動手開發插件化方案。
按照需要插件化主要解決下面的幾種問題:
1. 代碼的加載
(1) 要解決純Java代碼的加載
(2) Android組件加載,如Activity、Service、Broadcast Receiver、ContentProvider,因為它們是有生命周期的,所以要特殊處理
(3) Android Native代碼的加載
(4) Android 特殊控件的處理,如Notification等
2. 資源加載
不同插件的資源如何管理,是公用一套還是插件獨立管理?
因為在Android中訪問資源,都是通過R. 實現的,
下面就一步步解決上面的問題
1. 純Java代碼的加載
主要就是通過ClassLoader、更改DexElements將插件的路徑添加到原來的數組中。
詳細的分析可以參考我轉載的一篇文章,因為感覺原貼命名和結構有點亂,所以轉載記錄下。
https://my.oschina.net/android520/blog/794715
Android提供DexClassLoader和PathClassLoader,都繼承BaseDexClassLoader,只是構造方法的參數不一樣,即optdex的路徑不一樣,源碼如下
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader.java
public class PathClassLoader extends 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);
}
}
其中,optimizedDirectory是用來存儲opt后的dex目錄,必須是內部存儲路徑。
DexClassLoader可以加載外部的dex或apk,只要opt的路徑通過參數設置一個內部存儲路徑即可。
PathClassLoader只能加載已安裝的apk,因為opt路徑會使用默認的dex路徑,外部的不可以。
下面介紹下如何通過DexClassLoader實現加載Java代碼,參考Nuwa
這種方式類似于熱修復,如果插件和宿主代碼有相互訪問,則需要在打包中使用插樁技術實現。
public static boolean injectDexAtFirst(String dexPath, String dexOptPath) {
// 獲取系統的dexElements
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
// 獲取patch的dexElements
DexClassLoader patchDexClassLoader = new DexClassLoader(dexPath, dexOptPath, dexPath, getPathClassLoader());
Object patchDexElements = getDexElements(getPathList(patchDexClassLoader));
// 組合最新的dexElements
Object allDexElements = combineArray(patchDexElements, baseDexElements);
// 將最新的dexElements添加到系統的classLoader中
Object pathList = getPathList(getPathClassLoader());
FieldUtils.writeField(pathList, "dexElements", allDexElements);
}
public static ClassLoader getPathClassLoader() {
return DexUtils.class.getClassLoader();
}
/**
* 反射調用getPathList方法,獲取數據
* @param classLoader
* @return
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public static Object getPathList(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return FieldUtils.readField(classLoader, "pathList");
}
/**
* 反射調用pathList對象的dexElements數據
* @param pathList
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
LogUtils.d("Reflect To Get DexElements");
return FieldUtils.readField(pathList, "dexElements");
}
/**
* 拼接dexElements,將patch的dex插入到原來dex的頭部
* @param firstElement
* @param secondElement
* @return
*/
public static Object combineArray(Object firstElement, Object secondElement) {
LogUtils.d("Combine DexElements");
// 取得一個數組的Class對象, 如果對象是數組,getClass只能返回數組類型,而getComponentType可以返回數組的實際類型
Class objTypeClass = firstElement.getClass().getComponentType();
int firstArrayLen = Array.getLength(firstElement);
int secondArrayLen = Array.getLength(secondElement);
int allArrayLen = firstArrayLen + secondArrayLen;
Object allObject = Array.newInstance(objTypeClass, allArrayLen);
for (int i = 0; i < allArrayLen; i++) {
if (i < firstArrayLen) {
Array.set(allObject, i, Array.get(firstElement, i));
} else {
Array.set(allObject, i, Array.get(secondElement, i - firstArrayLen));
}
}
return allObject;
}
使用上面的方式啟動的Activity,是有生命周期的,應該是使用系統默認的創建Activity方式,而不是自己new Activity對象,所以打開的Activity生命周期正常。
但是上面的方式,必須保證Activity在宿主AndroidManifest.xml中注冊。
2. 下面介紹下如何加載未注冊的Activity功能
Activity的加載原理參考 https://my.oschina.net/android520/blog/795599
主要通過Hook系統的IActivityManager完成
3. 資源加載
資源訪問都是通過R.方式,實際上Android會生成一個0x7f******格式的int常量值,關聯對應的資源。
如果資源有更改,如layout、id、drawable等變化,會重新生成R.java內容,int常量值也會變化。
因為插件中的資源沒有參與宿主程序的資源編譯,所以無法通過R.進行訪問。
具體原理參照 https://my.oschina.net/android520/blog/796346
使用addAssetPath方式將插件路徑添加到宿主程序后,因為插件是獨立打包的,所以資源id也是從1開始,而宿主程序也是從1開始,可能會導致插件和宿主資源沖突,系統加載資源時以最新找到的資源為準,所以無法保證界面展示的是宿主的,還是插件的。
針對這種方式,可以在打包時,更改每個插件的資源id生成的范圍,可以參考public.xml介紹。
代碼參考Amigo
public static void loadPatchResources(Context context, String apkPath) throws Exception {
AssetManager newAssetManager = AssetManager.class.newInstance();
invokeMethod(newAssetManager, "addAssetPath", apkPath);
invokeMethod(newAssetManager, "ensureStringBlocks");
replaceAssetManager(context, newAssetManager);
}
private static void replaceAssetManager(Context context, AssetManager newAssetManager)
throws Exception {
Collection<WeakReference<Resources>> references;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Object resourcesManager = invokeStaticMethod(resourcesManagerClass, "getInstance");
if (getField(resourcesManagerClass, "mActiveResources") != null) {
ArrayMap<?, WeakReference<Resources>> arrayMap =
(ArrayMap) readField(resourcesManager, "mActiveResources", true);
references = arrayMap.values();
} else {
references = (Collection) readField(resourcesManager, "mResourceReferences", true);
}
} else {
HashMap<?, WeakReference<Resources>> map =
(HashMap) readField(ActivityThreadCompat.instance(), "mActiveResources", true);
references = map.values();
}
AssetManager assetManager = context != null ? context.getAssets() : null;
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources == null) continue;
try {
writeField(resources, "mAssets", newAssetManager);
originalAssetManager = assetManager;
} catch (Throwable ignore) {
Object resourceImpl = readField(resources, "mResourcesImpl", true);
writeField(resourceImpl, "mAssets", newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources == null) continue;
// android.util.Pools$SynchronizedPool<TypedArray>
Object typedArrayPool = readField(resources, "mTypedArrayPool", true);
// Clear all the pools
while (invokeMethod(typedArrayPool, "acquire") != null) ;
}
}
}
來自:https://my.oschina.net/android520/blog/796350