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);
- }
- }
- }
来源: http://www.tuicool.com/articles/uuyQ73J