Android(安卓)ClassLoader
类关系
与 java 的 CL 类似,但 Android 重新封装了一层 BaseCL ,类结构如下:
Android CL
同样,Android 的 CL 执行的也是父委托机制。
整体流程
-
DexCL 或 PathCL 构造函数中,会调用他们的父类 BaseDexCL 构造函数;
- BaseDexCL 会调用 ClassLoader 的构造函数。在 CL 的构造函数中,会将传入的 parent 参数值赋值给 ClassLoader 类中的 parent 属性中;
BaseDexCL 构造函数中会创建 DexPathList 对象,并且 BaseDexCL#findClass() 方法也是由 DexPathList#findClass() 实现。也就是说:BaseDexCL 的加载类的工作全部转移到 DexPathList 类完成;
-
DexPathList 构造函数中,会根据 DexCL 与 PathCL 构造函数中的 dexPath 值创建一个 Element[] 。
每一个 Element 都封装了对应路径的一些信息。
Element 中有一个重要的 DexFile 类型成员变量 。
DexPathList#findClass() 的实现就是遍历 Element[],并调用每一个 Element 对象中的 DexFile 对象的 loadClassBinaryName() 方法。
经过上面三步,BaseDexCL 加载类的工作转换到 DexFile 的 loadClasBinaryName() 方法,而该方法最终借由 native 实现。至此,整个 CL 加载类的流程在 JAVA 层的代码执行完毕。
动态加载
有两种思路:
可以借由 DexCL 加载指定目录下的 dex 文件。
可以通过反射获取 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); }
参数解释
dexPath : 包含 dex 文件的 .apk ,.zip 或 .jar 文件的路径。可以指定多个路径,各路径之前通过 File.pathSeparator 分隔即可。
optimizedDirectory : 存储解压出来的 dex 文件的目录;
libraryPath:动态库地址
parent : 父加载器。最终会成为 ClassLoader 类中的 parent 属性的值。
功能
可以从包含 classes.dex 的 .jar 或 .apk 文件中加载类的类加载器,因此可以执行未包含在应用程序内的代码。
-
该类加载器需要一个应用程序专用的可写目录来缓存优化的 classes 。可通过如下代码获取:
File dexOutputDir = context.getDir("dex", 0);
不要将目录设置到外部存储器上,因为外部存储器无法保证访问权限,从而无法避免代码遭受入侵。
注意事项
-
注意权限问题。如果将 .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); }
总结
- 有人说,PathCL 只能加载已安装的应用,DexCL 可以加载未安装的应用。但 6.0 上是可以加载的,4.4不行。故且这么记吧。
更多相关文章
- C语言函数的递归(上)
- android 数据存储之 SharedPreference
- Android(安卓)反编译
- XposedHook:hook敏感函数
- 从Android到React Native开发(一、入门)
- Android关于分包方案、插件化动态加载APK或DEX 以及热补丁资料总
- Android学习路线(二十七)键值对(SharedPreferences)存储
- Android(安卓)资源加载与匹配
- 两个Android选择文件对话框