类关系

与 java 的 CL 类似,但 Android 重新封装了一层 BaseCL ,类结构如下:


Android CL

同样,Android 的 CL 执行的也是父委托机制

整体流程

  1. DexCL 或 PathCL 构造函数中,会调用他们的父类 BaseDexCL 构造函数;

    • BaseDexCL 会调用 ClassLoader 的构造函数。在 CL 的构造函数中,会将传入的 parent 参数值赋值给 ClassLoader 类中的 parent 属性中;
  2. BaseDexCL 构造函数中会创建 DexPathList 对象,并且 BaseDexCL#findClass() 方法也是由 DexPathList#findClass() 实现。也就是说:BaseDexCL 的加载类的工作全部转移到 DexPathList 类完成

  3. DexPathList 构造函数中,会根据 DexCL 与 PathCL 构造函数中的 dexPath 值创建一个 Element[] 。

    • 每一个 Element 都封装了对应路径的一些信息。

    • Element 中有一个重要的 DexFile 类型成员变量 。

    • DexPathList#findClass() 的实现就是遍历 Element[],并调用每一个 Element 对象中的 DexFile 对象的 loadClassBinaryName() 方法。

  4. 经过上面三步,BaseDexCL 加载类的工作转换到 DexFile 的 loadClasBinaryName() 方法,而该方法最终借由 native 实现。至此,整个 CL 加载类的流程在 JAVA 层的代码执行完毕。

动态加载

有两种思路:

  1. 可以借由 DexCL 加载指定目录下的 dex 文件。

  2. 可以通过反射获取 BaseDexCL 类中的 DexPathList 对象,并通过反射修改该对象的 Element[],将自己要加载的 dex 文件转成 Element 对象,并添加到 Element[] 中。


DexCL

源码

只有一个构造函数,可以通过 DexCL 查看

    public DexClassLoader(String dexPath, String optimizedDirectory,            String libraryPath, ClassLoader parent) {        super(dexPath, new File(optimizedDirectory), libraryPath, parent);    }

参数解释

  1. dexPath : 包含 dex 文件的 .apk ,.zip 或 .jar 文件的路径。可以指定多个路径,各路径之前通过 File.pathSeparator 分隔即可。

  2. optimizedDirectory : 存储解压出来的 dex 文件的目录;

  3. libraryPath:动态库地址

  4. parent : 父加载器。最终会成为 ClassLoader 类中的 parent 属性的值。

功能

可以从包含 classes.dex 的 .jar 或 .apk 文件中加载类的类加载器,因此可以执行未包含在应用程序内的代码

  1. 该类加载器需要一个应用程序专用的可写目录来缓存优化的 classes 。可通过如下代码获取:

    File dexOutputDir = context.getDir("dex", 0);
  2. 不要将目录设置到外部存储器上,因为外部存储器无法保证访问权限,从而无法避免代码遭受入侵。

注意事项

  1. 注意权限问题。如果将 .apk 或 .jar 文件放置放置在需要读写权限的目录,而且尚未动态申请这些权限,就会报如下错误:

    No original dex files found for dex

只需要将相应的文件移植到 app 私有目录下即可。

示例

        File out = new File(getExternalCacheDir(),"out");//该位置是 app 私有的外部存储位置,不需要申请权限        if (!out.exists())            out.mkdirs();        File dex = new File(out,"xx.apk");// .apk 存储在该目录下        DexClassLoader dcl = new DexClassLoader(dex.getAbsolutePath(),out.getAbsolutePath(),null,getClassLoader());        try {            //加载指定的类后,可以通过反射调用其中的类,方法            Class<?> loadClass = dcl.loadClass("com.Test");            Object o = loadClass.newInstance();            Method test = loadClass.getDeclaredMethod("test",String.class);            test.setAccessible(true);            test.invoke(o,"------");        } catch (Exception e) {            e.printStackTrace();        }

执行完毕后,可以发现在 out 目录下生成了 xx.dex 文件。


PathCL

系统默认的类加载器。通过 Context#getClassLoader() 方法得到的就是 PathCL 对象。

源码

public PathClassLoader(String dexPath, ClassLoader parent) {    super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String libraryPath,    ClassLoader parent) {        super(dexPath, null, libraryPath, parent);}

可以发现,其与 DexCL 的区别在于:PathCL 的 optimizedDirectory 为 null。

在4.4 手机上,PathCL 无法加载未安装的 .apk,但6.0上可以。


BaseDexCL

方法解析

两个重要方法:构造函数以及 findClass() 如下:

    private final DexPathList pathList;    public BaseDexClassLoader(String dexPath, File optimizedDirectory,            String libraryPath, ClassLoader parent) {        super(parent);        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);    }    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        List suppressedExceptions = new ArrayList();        Class c = pathList.findClass(name, suppressedExceptions);        ……        return c;    }

从中可以看出,BaseDexCL 将它的所有重要操作都转移给其成员变量 pathList 执行。


DexPathList

构造函数

其构造函数主要是为一些成员变量进行初始化,其中最主要的是 dexElements 。

    public DexPathList(ClassLoader definingContext, String dexPath,            String libraryPath, File optimizedDirectory) {        //非空以及读写权限判断,代码略        this.definingContext = definingContext;        ArrayList suppressedExceptions = new ArrayList();        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,                                           suppressedExceptions);        if (suppressedExceptions.size() > 0) {            this.dexElementsSuppressedExceptions =                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);        } else {            dexElementsSuppressedExceptions = null;        }        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);    }

splitDexPath 是将 dexPath 按冒号分隔得到路径,并转成 ArrayList 返回。

maxDexElements如下:

        private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,                                             ArrayList suppressedExceptions) {        ArrayList elements = new ArrayList();        /*         * 遍历所有文件,并提取 dex 文件。         */        for (File file : files) {            File zip = null;            DexFile dex = null;            String name = file.getName();            if (file.isDirectory()) {//为文件夹时,直接存储                elements.add(new Element(file, true, null, null));            } else if (file.isFile()){                if (name.endsWith(DEX_SUFFIX)) {                    // loadDexFile 的作用是:根据 file 获取对应的 DexFile 对象。                    try {                        dex = loadDexFile(file, optimizedDirectory);                    }                     // 异常处理部分 略                } else {                    zip = file; // 非 dex 文件,那么 zip 表示包含 dex 文件的压缩文件,如 .apk,.jar 文件等                    try {                        dex = loadDexFile(file, optimizedDirectory);                    }                    // 异常处理部分 略                }            }            ……            //若 file 为目录,zip 与 dex 都为 null ,不会执行该步。所以同一个file 只会生成一个 Element 对象。            if ((zip != null) || (dex != null)) {                elements.add(new Element(file, false, zip, dex));            }        }        return elements.toArray(new Element[elements.size()]);    }

其中 loadDexFile 如下:

    private static DexFile loadDexFile(File file, File optimizedDirectory)            throws IOException {        if (optimizedDirectory == null) {            return new DexFile(file);        } else {            String optimizedPath = optimizedPathFor(file, optimizedDirectory);            return DexFile.loadDex(file.getPath(), optimizedPath, 0);        }    }

optimizedPathFor() 会在 optimizedDirectory 目录下生成一个与 file 同名的 dex 文件。所以如果指定了 optimizedDirectory ,则生成的 dex 文件会存储到指定的目录中。

上述几个步骤的核心作用:将 File 对象转换成 Element 对象,并最终形成 Element 数组,然后赋值给 DexPathList 的成员变量 dexElements

findClass()

DexPathList类中另一个重要方法 findClass 源码如下:

    public Class findClass(String name, List suppressed) {        for (Element element : dexElements) {            DexFile dex = element.dexFile;            if (dex != null) {                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);                if (clazz != null) {                    return clazz;                }            }        }        ……        return null;    }

结合下面 Element 类的源码,可以知道 element.dexFile 就是 loadDexFile() 方法的返回值。

而 findClass 的实现,最终是通过 DexFile#loadClassBinaryName() 完成的。因此,DexClassLoader和PathClassLoader其实都是通过DexFile这个类来实现类加载的。

findClass() 遍历了之前所有的 DexFile 实例,也就是遍历了所有加载过的 dex
文件,再调用 loadClassBinaryName() 一个个尝试看能不能加载到想要的类

因此,可以向 dexElements 中添加额外的 DexFiles 对象,达到动态加载 dex 文件的目的

Element 类

每一个 Element 对象都封装了 dexPath 中各种路径所对应的信息

Element 对象的初始化在 makeDexElements() 中进行。其对应的构造函数如下:

        private final File file;        private final boolean isDirectory;        private final File zip;        private final DexFile dexFile;        public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {            this.file = file;            this.isDirectory = isDirectory;            this.zip = zip;            this.dexFile = dexFile;        }

由 makeDexElements() 函数中调用可知,构造函数中的四个参数含义如下:

  • file 指的是外界传递的 dexPath 经冒号分隔后,各路径对应的 File 对象。

  • isDirectory 记录 file 是不是目录,如果是目录,则该值为 true;否则为 false。

  • zip 表示包含 dex 文件的文件。如果 file 是目录或者 file 本身就是 dex 文件,则该属性的值为 null;

  • dexFile 为 dex 文件对应的 DexFile 对象,其值由 loadDexFile() 函数产生。

DexFile

以下为 loadDexFile 源码,就是在该方法中引入了 DexFile 类。

    private static DexFile loadDexFile(File file, File optimizedDirectory)            throws IOException {        if (optimizedDirectory == null) {            return new DexFile(file);        } else {            // optimizedPathFor 方法会在 optimizedDirectory 目录下建一个与 file 同名的 dex 文件            String optimizedPath = optimizedPathFor(file, optimizedDirectory);            return DexFile.loadDex(file.getPath(), optimizedPath, 0);        }    }

我们知道,PathCL 的 optimizedDirectory 为 null ,DexCL 的 optimizedDirectory 不为 null。

上述两个代码最终会殊途同归到 openDexFile 方法,只不过 PathCL 的第二个参数为 null ,而 DexCL 的第二个参数不为 null:

    private static Object openDexFile(String sourceName, String outputName, int flags) throws IOException {        // 下一步是调用 native 方法        return openDexFileNative(new File(sourceName).getAbsolutePath(),                                 (outputName == null) ? null : new File(outputName).getAbsolutePath(),                                 flags);    }

总结

  1. 有人说,PathCL 只能加载已安装的应用,DexCL 可以加载未安装的应用。但 6.0 上是可以加载的,4.4不行。故且这么记吧。

更多相关文章

  1. C语言函数的递归(上)
  2. android 数据存储之 SharedPreference
  3. Android(安卓)反编译
  4. XposedHook:hook敏感函数
  5. 从Android到React Native开发(一、入门)
  6. Android关于分包方案、插件化动态加载APK或DEX 以及热补丁资料总
  7. Android学习路线(二十七)键值对(SharedPreferences)存储
  8. Android(安卓)资源加载与匹配
  9. 两个Android选择文件对话框

随机推荐

  1. R语言XML格式数据导入与处理 - ShangFR
  2. xml学习(1)xml的几种文件格式
  3. XML—XML解析之DOM
  4. XML Http Request最新替代技术—— Fetch
  5. Java&Xml教程(十一)JAXB实现XML与Java对象
  6. php使用simplexml来解析xml
  7. Java对象、Json、Xml转换工具Jackson使用
  8. XML—XML文件约束之DTD详解
  9. 疯狂XML学习笔记(13)---------XML DOM
  10. Java&Xml教程(十)XML作为属性文件使用