曾几何时,国内各大公司掀起了一股研究Android动态加载的技术,两年多过去了,动态加载技术俨然成了Android开发中必须掌握的技术。那么动态加载技术是什么呢,这里谈谈我的个人看法,如有雷同,纯属偶然。
什么是动态加载技术
JVM 类加载机制
- BootStrapClassLoader是顶级的类加载器,它是唯一一个不继承自ClassLoader中的类加载器,它高度集成于 JVM是ExtensionClassLoader的父加载器,它的类加载路径是JDK/jre/lib 和 用户指定的虚拟机参数-Xbootclasspath的值。
- ExtensionClassLoader 是 BootStrapClassLoader 的子加载器,同时是 SystemClassLoader(有的地方称 AppClassLoader)的父加载器,它的类加载路径是 JDK/jre/lib/ext 和系统属性 java.ext.dirs 的值。
- SystemClassLoader 是 ExtensionClassLoader 的子加载器,同时是我们的应用程序的类加载器,我们在应用程序中编写的类一般情况下(如果没有到动态加载技术的话)都是通过这个类加载加载的。它的类加载路径是环境变量 CLASSPATH 的值或者用户通过命令行可选项 -cp (-classpath) 指定的值。
- 类加载器由于父子关系形成树形结构,开发人员可以开发自己的类加载器从而实现动态加载功能,但必须给这个类加载器指定树上的一个节点作为它的父加载器。
- 因为类加载器是通过包名和类名(或者说类的全限定名),所以由于委派式加载机制的存在,全限定名相同的类不会在有 祖先—子孙 关系的类加载器上分别加载一次,不管这两个类的实现是否一样。
- 不同的类加载器加载的类一定是不同的类,即使它们的全限定名一样。如果全限定名一样,那么根据上一条,这两个类加载器一定没有 祖先-子孙 的关系。这样来看,可以通过自定义类加载器使得相同全限定名但实现不同的类存在于同一 JVM 中,也就是说,类加载器相当于给类在包名之上又加了个命名空间。
- 如果两个相同全限定名的类由两个非 祖先-子孙 关系的类加载器加载,这两个类之间通过instanceof 和 equals() 等进行比较时总是返回false。
安卓应用和普通的 java 应用不同,它们运行于 Dalvik 虚拟机。JVM 是基于栈的虚拟机,而 Dalvik 是基于寄存器的虚拟机。Android采用 dex 作为储存类字节码信息的文件。当 java 程序编译成 class 后,编译器会使用 dx 工具将所有的class 文件整合到一个 dex 文件,目的是使其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加紧凑。
DexClassLoader & PathClassLoader说明
package dalvik.system; import java.io.File; public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
package dalvik.system; 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); } }
可以看到,这两个类加载器都是继承自 BaseDexClassLoader,只是分别实现了自己的构造方法。
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory); }
我们发现BaseDexClassLoader作为一个基类,其构造极其简单,它做了两件事:连接了父加载器;构造了一个 DexPathList 实例保存在 pathList 中。
- 第一个参数指的是我们要加载的 dex 文件的路径,它有可能是多个 dex 路径,取决于我们要加载的 dex 文件的个数,多个路径之间用 : 隔开。
- 第二个参数指的是优化后的 dex 存放目录。实际上,dex 其实还并不能被虚拟机直接加载,它需要系统的优化工具优化后才能真正被利用。优化之后的 dex 文件我们把它叫做 odex (optimized dex,说明这是被优化后的 dex)文件。其实从 class 到 dex 也算是经历了一次优化,这种优化的是机器无关的优化,也就是说不管将来运行在什么机器上,这种优化都是遵循固定模式的,因此这种优化发生在 apk 编译。而从 dex 文件到 odex 文件,是机器相关的优化,它使得 odex 适配于特定的硬件环境,不同机器这一步的优化可能有所不同,所以这一步需要在应用安装等运行时期由机器来完成。需要注意的是,在较早版本的系统中,这个目录可以指定为外部存储中的目录,较新版本的系统为了安全只允许其为应用程序私有存储空间(/data/data/apk-package-name/)下的目录,一般我们可以通过 Context#getDir(String dirName) 得到这个目录。
- 第三个参数的意义是库文件的的搜索路径,一般来说是 .so 库文件的路径,也可以指明多个路径。
- 第四个参数就是要传入的父加载器,一般情况我们可以通过 Context#getClassLoader() 得到应用程序的类加载器然后把它传进去。
好了,到这里就很清楚了,Dalvik 虚拟机要加载的 dex 文件的路径(DexPathList),那么
Dalvik是如何找到Dex的呢?有人会说反射,对,大方向对了。那么我们看看系统究竟是怎么做的。
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); // Native libraries may exist in both the system and // application library paths, and we use this search order: // // 1. This class loader's library path for application libraries (librarySearchPath): // 1.1. Native library directories // 1.2. Path to libraries in apk-files // 2. The VM's library path from the system property for system libraries // also known as java.library.path // // This order was reversed prior to Gingerbread; see http://b/2933456. this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, suppressedExceptions, definingContext); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } }
这里我们主要看如下几行代码:
his.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); // Native libraries may exist in both the system and // application library paths, and we use this search order: // // 1. This class loader's library path for application libraries (librarySearchPath): // 1.1. Native library directories // 1.2. Path to libraries in apk-files // 2. The VM's library path from the system property for system libraries // also known as java.library.path // // This order was reversed prior to Gingerbread; see http://b/2933456. this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, suppressedExceptions, definingContext);
这段代码主要是给 dexElements和nativeLibraryPathElements赋值。我们知道Android在通过默认的虚拟机dex后,会继续优化为odex 文件。
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
List<File> result = new ArrayList<>();
if (searchPath != null) {
for (String path : searchPath.split(File.pathSeparator)) {
if (directoriesOnly) {
try {
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode)) {
continue;
}
} catch (ErrnoException ignored) {
continue;
}
}
result.add(new File(path));
}
}
return result;
}
这个方法很简单就是用,分隔的路径分割后保存为 File 类型的列表返回。现在看看
makeDexElements()
这个方法:
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
ClassLoader loader) {
return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
}
private static Element[] makeElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, boolean ignoreDexFiles, ClassLoader loader) { Element[] elements = new Element[files.size()]; int elementsPos = 0; /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { File zip = null; File dir = new File(""); DexFile dex = null; String path = file.getPath(); String name = file.getName(); if (path.contains(zipSeparator)) { String split[] = path.split(zipSeparator, 2); zip = new File(split[0]); dir = new File(split[1]); } else if (file.isDirectory()) { // We support directories for looking up resources and native libraries. // Looking up resources in directories is useful for running libcore tests. elements[elementsPos++] = new Element(file, true, null, null); } else if (file.isFile()) { if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { zip = file; if (!ignoreDexFiles) { try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ suppressedExceptions.add(suppressed); } } } } else { System.logW("ClassLoader referenced unknown path: " + file); } if ((zip != null) || (dex != null)) { elements[elementsPos++] = new Element(dir, false, zip, dex); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; }
通过代码我们可以大致了解到,这个方法就是将之前的File对象通过重新组合成一个新的Elements对象,然后我们Loader读取的就是Element对象。看一下 loadDexFile() 怎样加载 DexFile 的
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException { if (optimizedDirectory == null) { return new DexFile(file, loader, elements); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); } }
先说明下无论是
DexFile(File file, Classloader loader, Elements[] elements)
还是 DexFile.loadDex() 最终都会调用
DexFile(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements)
这个构造方法。所以这个方法的逻辑就是:如果
optimizedDirectory
为 null,那么就直接利用 file 的路径构造一个
DexFile
;否则就根据要加载的 dex(或者包含了 dex 的 zip) 的文件名和优化后的 dex 存放的目录组合成优化后的 dex(也就是 odex)文件的输出路径,然后利用原始路径和优化后的输出路径构造出一个
DexFile.
为了加深大家对DexPathList的理解,我们来看看官方的说明。
BaseClassLoader 加载器的类加载过程
我们知道,一个类加载器的入口方法是
loadClass()。这是Java语音所共有的。类加载器通过findClass()找到所需要加载的类。
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
BaseDexClassLoader 也继承自 ClassLoader,因此我们就从 findClass() 方法来分析下 BaseClassLoader 加载类的过程。
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class /"" + name + "/" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; }
这个方法极其简单,主要风格findclass找到类
Class c = pathList.findClass(name, suppressedException)
这里
BaseClassLoader
把查找类的任务委托给了
pathList
。那么我们来看一下Android的DexPathList的findClass又做了什么事情。
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
它遍历了
dexElements
中的所有
DexFile
,
通过
DexFile
的
loadClassBinaryName()
方法加载目标类
。dexElements 又把查找类的任务委托给了DexFile
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException { if (outputName != null) { try { String parent = new File(outputName).getParent(); if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) { throw new IllegalArgumentException("Optimized data directory " + parent + " is not owned by the current user. Shared storage cannot protect" + " your application from code injection attacks."); } } catch (ErrnoException ignored) { // assume we'll fail with a more contextual error later } } mCookie = openDexFile(sourceName, outputName, flags, loader, elements); mFileName = sourceName; //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName); }
到这里我们就已经很明白了,openDexFile调用openDexFileNative()方法,(
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
它做的事就是把对应的 dex 文件加载到内存中,然后返回给 java 层一个类似句柄一样的东西
Object:mCookie。
在构造方法中
DexFile
就完成了 dex 文件的加载过程。现在我们回到
DexFile
对象的
loadClassBinaryName()
:
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
看到这里我们明白了,class 对象在 java 层加载过程的尽头就是这个 defineClass() 方法,这个方法调用本地法 defineClassNative() 从 dex 中查找目标类,如果找到了,就把这个代表这个类的 Class 对象返回。
到此,Android的加载过程我们终于看完了。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5836.html