自己动手实现Android App插件化

来源:互联网 发布:c语言初级题 编辑:程序博客网 时间:2024/06/05 10:30

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.javapublic class DexClassLoader extends BaseDexClassLoader {    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {        super(dexPath, new File(optimizedDirectory), libraryPath, parent);    }}// PathClassLoader.javapublic 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;}

2. 下面介绍下如何加载Activity,处理生命周期

使用上面的方式启动的Activity,是有生命周期的,应该是使用系统默认的创建Activity方式,而不是自己new Activity对象,所以打开的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介绍。

0 0
原创粉丝点击