Android插件化Step 2 - 插件加载机制

本文主要参考借鉴了weishu的文章,weishu在博客中讲述了android插件化的一系列文章,写的很好,只是他的代码分析是基于Android6.0上的,Android8.0无法适用,所以本文针对这一部分做了修改。

在上一篇文章中讲述了如何启动没有在AndroidManifest.xml中显式声明的Activity,通过Hook AMS和拦截ActivityThread中H类对于组件调度成功地绕过了AndroidMAnifest.xml的限制。但是我们启动的『没有在AndroidManifet.xml中显式声明』的Activity和宿主程序存在于同一个Apk中;通常情况下,插件均以独立的文件存在甚至通过网络获取,这时候插件中的Activity应该怎么样启动?

系统通过ClassLoader加载了需要的Activity类并通过反射调用构造函数创建出了Activity对象。如果Activity组件存在于独立于宿主程序的文件之中,系统的ClassLoader怎么知道去哪里加载呢?因此,如果不做额外的处理,插件中的Activity对象甚至都没有办法创建出来,谈何启动?

因此,要使存在于独立文件或者网络中的插件被成功启动,首先就需要解决这个插件类加载的问题。

下文将围绕此问题展开,完成『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

ClassLoader类加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化的,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。与那些在编译时进行链连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以同代拓展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个面相接口的应用程序,可以等到运行时在制定实际的实现类;用户可以通过Java与定义的和自定义的类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为代码的一部分,这种组装应用程序的方式目前已经广泛应用于Java程序之中。从最基础的Applet,JSP到复杂的OSGi技术,都使用了Java语言运行期类加载的特性。

Java的类加载是一个相对复杂的过程;它包括加载、验证、准备、解析和初始化五个阶段;对于开发者来说,可控性最强的是加载阶段;加载阶段主要完成三件事:

  1. 根据一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为JVM方法区中的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

『通过一个类的全限定名获取描述此类的二进制字节流』这个过程被抽象出来,就是Java的类加载器模块,也即JDK中ClassLoader API。Android Framework提供了DexClassLoader这个类,简化了『通过一个类的全限定名获取描述次类的二进制字节流』这个过程;我们只需要告诉DexClassLoader一个dex文件或者apk文件的路径就能完成类的加载。因此本文的内容用一句话就可以概括:
将插件的dex或者apk文件告诉『合适的』DexClassLoader,借助它完成插件类的加载。
不熟悉的可以看下这个文章classloader.

具体实现

明确了目的其实方式也很简单。就是将插件的dex或者apk加入宿主classloader的路径中,这样宿主开启插件应用时就会顺着路径直接找到插件的apk然后加载启动。(这里讲了大概原理,具体分析可以移步至weishu的博客,下有链接。)

public final class BaseDexClassLoaderHookHelper {

    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 获取 BaseDexClassLoader : pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");

        // 获取 PathList: Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");

        // Element 类型
        Class<?> elementClass = dexElements.getClass().getComponentType();

        // 创建一个数组, 用来替换原始的数组
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
        Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
        Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
        Object o = RefInvoke.createObject(elementClass, p1, v1);

        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements复制进去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那个element复制进去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

        // 替换
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }
}

注意其中的这么几句话:

Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Object o = RefInvoke.createObject(elementClass, p1, v1);
Object[] toAddElementArray = new Object[] { o };

这几句话中,通过反射执行了Element的带有4个参数的构造函数,但不幸的是,在Android O以及之后的版本,这个带有4个参数的构造函数就被废弃了。
此外,在这个构造函数中使用到的DexFile这个类,也被废弃了,对此Google给出的解释是,只有Android系统可以使用DexFile,App层面不能使用它。
于是,我们不得不另辟蹊径,通过执行DexPathList类的makeDexElements方法,来生成插件中的dex:

List<File> legalFiles = new ArrayList<>();
legalFiles.add(apkFile);

List<IOException> suppressedExceptions = new ArrayList<IOException>();

Class[] p1 = {List.class, File.class, List.class, ClassLoader.class};
Object[] v1 = {legalFiles, optDexFile, suppressedExceptions, cl};
Object[] toAddElementArray = (Object[])
RefInvoke.invokeStaticMethod("dalvik.system.DexPathList", "makeDexElements", p1, v1);

小结

  1. 默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。
  2. 宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。

最后, 同学点个赞吧!!! 加个关注好么
Android 插件化原理解析——插件加载机制

推荐阅读更多精彩内容