主目录见:Android高级进阶知识(这是总目录索引)
在线源码查看:AndroidXRef
了解这一篇的知识对后面插件化中的类加载是必不可少的,我们知道,我们的应用中的类在编译过程中会被编译成dex文件,所以把我们的dex加载进我们的程序我们就可以查找到插件中的类。我们今天就会来了解这一过程。
一.类加载器
学过java的应该知道,我们的类是通过类加载器加载到JVM的,Android也不例外,Android中有两个特别重要的ClassLoader:
1.PathClassLoader:
可以看到这边英文描述了这个类加载器不会加载网络上的类,只会加载系统类和应用类的,而且在dalvik虚拟机上只能加载已经安装的apk的dex。当然在android 5.0之后是否可以加载未安装的apk的dex,这个没做过实验。但是可以知道,用这个类加载器来加载插件中的dex是不可行的。我们看下完整的这个类:
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);
}
}
我们看到构造函数都是调用了super的构造函数,所以我们待会在看BaseDexClassLoader
时候会详细来说明。但是我们看第二个参数为空,这个参数是optimizedDirectory
,是dex文件被加载后会被编译器优化,优化之后的dex存放路径,因为PathClassLoader
只能加载系统类或者应用的类,所以这个为空,其实optimizedDirectory
为null时的默认路径就是/data/dalvik-cache 目录。
2.DexClassLoader
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getDir(String, int)} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getDir("dex", 0);
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
上面英文注释分别说明了三个方面的知识:
1).DexClassLoader
支持加载APK、DEX和JAR,也可以从SD卡进行加载。
上面说dalvik不能直接识别jar,DexClassLoader
却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader
里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件。
2).这个类需要提供一个optimizedDirectory
路径用于存放优化后的dex。
3).optimizedDirectory
路径不允许是外部存储的路径,为了防止应用被注入攻击。
我们来看下DexClassLoader完整的类:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
我们看到第二个参数传了不为null的目录,跟PathDexClassLoader不同,所以这个类加载器可以从外部存储里面加载apk,dex或者jar文件,我们目标就是它了。
3.BaseDexClassLoader
PathClassLoader
和DexClassLoader
都继承自BaseDexClassLoader
,其中的主要逻辑都是在BaseDexClassLoader
完成的。
可以看到构造函数会new出一个DexPathList对象,我们等会会说,现在我们先来看看参数的意思:
1).dexPath:待加载的类的apk,jar,dex的路径,必须是全路径。如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用
File.pathSeparato
r获得。上面"支持加载APK、DEX和JAR,也可以从SD卡进行加载"指的就是这个路径,最终做的是将dexPath路径上的文件ODEX优化到内部位置optimizedDirectory,然后,再进行加载的。2).libraryPath:目标类中所使用的C/C++库存放的路径,也就是so文件的路径。
3).ClassLoader:是指该装载器的父装载器,一般为当前执行类的装载器,例如在Android中以
context.getClassLoader()
作为父装载器。因为类加载器的双亲委托机制,需要设置一个父装载器。
二.类加载过程
我们知道,类的加载过程最终都会通过BaseDexClassLoader
中的findClass()
开始的:
可以看到我们这里的Class对象是通过pathList中的
findClass()
方法获取的。这里的pathList又是什么呢?这个类在上面BaseDexClassLoader
的构造函数中初始化的。我们可以看下:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, 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;
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, 121 suppressedExceptions);
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, 140 suppressedExceptions);
}
这个类很重要,我们可以看到首先是dexPath,optimizedDirectory的非空判断。然后是dexElements
的赋值,这里我们说下dexElements
:
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934). */
private final Element[] dexElements;
这个数组就是放我们dex的数组,我们的不同的dex作为数组存放。里面注释很有意思有句话,FaceBook使用反射来修改dexElements
,很明确告诉我们也可以通过修改这个数组来加载我们的dex。接着我们来看看makePathElements
方法,在看这个方法之前我们看到里面有个参数是调用splitDexPath
方法,这个方法是用于根据分隔符取到文件列表的:
private static Element[] makePathElements(ArrayList<File> files,
File optimizedDirectory) {
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.add(new Element(file, true, null, null));
} else if (file.isFile()) {
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
......
}
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
这个方法就是遍历之前得到的文件列表,然后判断传进来的文件是目录,zip文件或者dex文件,如果是目录的话,直接将文件file作为参数传给Element然后添加进elements中。否则其他情况都会调用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);
}
}
我们看到这个方法很简单,如果optimizedDirectory == null则直接new 一个DexFile
,否则就使用DexFile#loadDex
来创建一个DexFile
实例。
private static String optimizedPathFor(File path,
File optimizedDirectory) {
/*
* Get the filename component of the path, and replace the
* suffix with ".dex" if that's not already the suffix.
*
* We don't want to use ".odex", because the build system uses
* that for files that are paired with resource-only jar
* files. If the VM can assume that there's no classes.dex in
* the matching jar, it doesn't need to open the jar to check
* for updated dependencies, providing a slight performance
* boost at startup. The use of ".dex" here matches the use on
* files in /data/dalvik-cache.
*/
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
这个方法获取被加载的dexpath
的文件名,如果不是“.dex”结尾的就改成“.dex”结尾,然后用optimizedDirectory
和新的文件名构造一个File并返回该File的路径,所以DexFile#loadDex
方法的第二个参数其实是dexpath文件对应的优化文件的输出路径。
接着 DexPathList
构造函数中会获取so文件库的路径,然后传给makePathElements
方法,同样地,也是添加到DexElement中,到这里我们已经将我们的类和so文件添加进DexElement数组中了。所以我们插件化框架只要将我们的插件中的类想办法添加进DexElement数组中就可以了。
然后我们继续分析我们DexPathList#findClass()
:
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
我们看到这里会遍历我们的dexElements数组,然后取出数组中的DexFile
对象,调用他的DexFile#loadClassBinaryName
方法:
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
我们看到最终查找类会调用到native的defineClass()方法。这样,我们的加载流程就算讲完了。
总结:到这里类的加载已经讲完了,这里只是说明了一下流程,希望对大家会有点帮助,这也是插件化中很重要的一步。