n1cef1sh's Blog

前言

学完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

实例测试

见另篇动态加载笔记。

参考来源

Android类加载机制的细枝末节

类加载机制系列2——深入理解Android中的类加载器