Android源码学习——ClassLoader(1)

时间:2021-06-04 19:38:30

本文学习的源码参考AndroidXRef,版本为Lollipop 5.1.0_r1。


类加载与动态加载

Java代码是通过Class来实现的,程序运行时虚拟机首先将对应的类加载到内存中,然后才能进行对象实例化以及后续工作,而这个加载过程就是通过ClassLoader来实现的,也即是类加载。
同时,在Java虚拟机中,我们可以自定义继承自ClassLoader的类加载器,通过defineClass方法动态加载一个Class,实现控制程序的类加载过程,提高程序的灵活性。

Android的Dalvik/ART虚拟机与Java虚拟机一样,采用类加载机制,也具有动态加载技术,但是他们之间还是存在一些差异的。

Android的类加载机制

不管是类加载还是动态加载,它的基础都是ClassLoader,也即专门用来处理类加载工作的,也称为类加载器。而且,一个应用不仅仅只有一个类加载器。

实际上,Android系统启动时,会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类,而Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。另外,APP也有自己实现的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。

可以通过下面的代码查看当前程序的类加载器:

    // 获取当前程序的类加载器
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
// 获取当前类加载器的父类加载器
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
}
}

Android中的ClassLoader是一个抽象类,实际开发过程中,我们一般是使用他的两个具体的子类DexClassLoader、PathClassLoader这些类加载器来加载类的。他们的主要差别在于:

  • PathClassLoader只能加载系统中已经安装过的apk;
  • DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;

看具体代码:

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);
}

}
public class DexClassLoader extends BaseDexClassLoader {

public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

}

这两个类加载器都只是简单的封装了BaseDexClassLoader,具体的实现还是调了父类里的方法。
但是可以看到,PathClassLoader的第二个参数optimizedDirectory只能是null。

继续看BaseDexClassLoader里面的实现:

public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

......

}

创建了一个DexPathList实例。

继续看DexPathList的构造函数的实现:

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;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
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);
}

先进行了一些验证,然后调用了makeDexElements函数。

    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files up front.
*/

for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();

if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only 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) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}

if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}

return elements.toArray(new Element[elements.size()]);
}

这段代码是为了生成一个元素为dex或resource的数组,如果传入参数是一个目录则直接new Element(file, true, null, null)添加到数组里面;
如果传入参数是一个文件,如.dex、.zip、.jar或.apk,会先执行loadDexFile再new Element(file, false, zip, dex)添加到数组中。
其中,不是直接给出.dex文件的情况下,会将file赋值给zip。

看下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带来的差异了。
如果optimizedDirectory为null,也即PathClassLoader的情况,程序会直接调DexFile的构造函数创建一个DexFile对象;
而DexClassLoader则先去调了一个optimizedPathFor,再去调DexFile的loadDex方法。

看下这个里面做了什么:

    private static String optimizedPathFor(File path,
File optimizedDirectory) {

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();
}

解析path中dex的文件名,在optimizedDirectory位置新建了一个文件,返回文件路径。

    static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {

return new DexFile(sourcePathName, outputPathName, flags);
}

调用DexFile的另一个构造函数,返回DexFile对象。

看下这两个构造函数的区别:

    public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
}

private DexFile(String sourceName, String outputName, int flags) 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);
mFileName = sourceName;
guard.open("close");
}

如果是DexClassLoader,会先检查输出路径outputName父目录的所有者。但是无论是哪种ClassLoader,最终都会调用openDexFile。

看下openDexFile的实现:

    private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {

return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null) ? null : new File(outputName).getAbsolutePath(),
flags);
}

而openDexFile会直接返回openDexFileNative,而openDexFile是一个native函数,后面再继续分析。