学完Java篇类加载机制后,想了想还是把Android篇单拿出一次笔记学习吧,因为很多原理要看它源码的实现细节才好理解,死记硬背感觉很僵硬,所以这篇的篇幅也会比较大。基本上是找到源码后,先自己理解再结合网上博文的解析,整合出一些笔记。在过程中,有几次豁然开朗的感觉,同样也有很多地方需要时间消化。
Java中的ClassLoader加载的是字节码文件.class,但是Android(Dalvik/Art)只能识别dex文件,所以Java里的类加载器不能直接拿过来用。与之类似,Android系统里也提供了三个类加载器。
图源https://www.jianshu.com/p/7193600024e7
上图表示了Android里的类加载器的继承结构。SecureClassLoader和UrlClassLoader是在Java中的类加载器,需要注意的是PathClassLoader和DexClassLoader都继承于BaseDexClassLoader。
看一下BaseDexClassLoader的源码。
public class BaseDexClassLoader extends ClassLoader {
// 需要加载的dex列表
private final DexPathList pathList;
// dexPath是加载的dex文件所在的路径
// optimizedDirectory是odex将dexPath目录下的dex优化后输出到的路径(必须是手机内部路径)
// 如果optimizedDirectory 为null则使用系统默认路径也就是/data/dalvik-cache/目录
// 这个目录一般情况下没有权限访问,所以我们只能用DexClassLoader去加载类而不用PathClassLoader。
// libraryPath是需要加载的C/C++库路径
// parent是父类加载器对象
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<Throwable> suppressedExceptions = new ArrayList<Throwable>();、
// 使用pathList对象查找name类
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
}
这里面有一个DexPathList,我们看看它内部是什么操作。
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private final ClassLoader definingContext;
private final Element[] dexElements;
// 本地库目录
private final File[] nativeLibraryDirectories;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
// 当前类加载器的父类加载器
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// 根据输入的dexPath创建dex元素对象
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);
}
}
这里创建了一个异常的list,给dexElements赋值,它是一个数组。
而splitDexPath对dexPath做了什么呢?同样的还有下面的splitLibraryPath(libraryPath)
private static ArrayList<File> splitDexPath(String path) {
return splitPaths(path, null, false);
}
private static File[] splitLibraryPath(String path) {
ArrayList<File> result = splitPaths(
path, System.getProperty("java.library.path", "."), true);
return result.toArray(new File[result.size()]);
}
他们都调用了splitPaths()。
private static ArrayList<File> splitPaths(String path1, String path2,
boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
创建了一个arraylist,最后返回的也是它。然后我们再最后看splitAndAdd做了什么事情。
private static void splitAndAdd(String path, boolean wantDirectories,
ArrayList<File> resultList) {
if (path == null) {
return;
}
String[] strings = path.split(Pattern.quote(File.pathSeparator));
for (String s : strings) {
File file = new File(s);
if (! (file.exists() && file.canRead())) {
continue;
}
/*
* Note: There are other entities in filesystems than
* regular files and directories.
*/
if (wantDirectories) {
if (!file.isDirectory()) {
continue;
}
} else {
if (!file.isFile()) {
continue;
}
}
resultList.add(file);
}
}
这里的把路径进行分割,而Java中的File.pathSeparator是指分隔多个路径的分隔符,在windows下是“;”而不是指目录之间的分隔符。目录之间的分隔符要使用File.separator 在windows下是反斜杠,而在linux下是斜杠。然后把分割后的路径变成file,加到之前创建的arraylist
追踪了一大堆源码,搞清楚了splitDexPath和splitLibraryPath的实现细节。那么我们回过头来再去看前面的DexPathList。剩下不清楚的就是makeDexElements了,但是他的三个参数我们知道是什么了,只需要看看他实现的操作。
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
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 if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* 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). Safe to just ignore
* the exception here, and let dex == null.
*/
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
看着挺长的,其实一多半是注释。结合着注释理解,就是把前面dexPath里面解析的路径下的文件全部遍历一遍,如果是dex文件或apk和jar文件就会查找它们内部的dex文件,将所有这些dex文件都加入到Element数组中,完成加载路径下面的所有dex解析。
同样的,哪里搞不懂,我们就追踪它的源码。我们先看看Element数组具体是什么样,它其实就是简单结构体加个方法,存储了file,zip,dex三个字段。
那么继续,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实例。至于optimizedPathFor这个方法获取被加载的dexpath的文件名,如果不是“.dex”结尾的就改成“.dex”结尾,然后用optimizedDirectory和新的文件名构造一个File并返回该File的路径。
而loadDex是这样的。
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
其实和直接new DexFile差不多,它内部也是返回了DexFile的构造方法。这里不再细究,只要区分一下两个调用。如果直接new DexFile的话,optimizedDirectory这个参数为null,构造器里有个openDexFile方法就会使用默认目录/data/dalvik-cache/,而后者optimizedDirectory则是指定的目录。
DexPathList的内部实现顺了一遍,最重要的就是调用了makeDexElements()方法创建了Elements[]数组,把能读取到的不为空的dex都作为元素加入到数组里。
最后是findClass()的操作。
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历Element数组
for (Element element : dexElements) {
// 获取DexFile,然后调用DexFile对象的loadClassBinaryName()方法来加载Class文件。
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;
}
DexPathList最终遍历自身的Element[]数组,获取DexFile对象来加载Class文件。
图源https://www.jianshu.com/p/3afa47e9112e
见另篇动态加载笔记。