Android 类加载源码分析(一)

1,251 阅读8分钟

概述

本篇将对Android的类加载机制进行分析。总体来说Android的ClassLoader分为系统ClassLoader和自定义的ClassLoader 系统的包括有三种:

  1. BootClassLoader Android系统启动时会使用BootClassLoader预加载一些类。它位于类加载器链的头部。
  2. PathClassLoader 可以加载已经安装的apk,即也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
  3. DexClassLoader,可以加载一个未安装的apk文件。

我们在App中使用的系统类加载器默认是PathClassLoader,它的父加载器是BootClassLoader。为什么是PathClassLoader呢?因为在contextImpl的getClassLoader有如下实现


// /frameworks/base/core/java/android/app/ContextImpl.java
@Override
public ClassLoader getClassLoader() {
    return mPackageInfo != null ?
            mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}

// /frameworks/base/core/java/android/app/LoadedApk.java
public ClassLoader getClassLoader() {
    synchronized (this) {
        if (mClassLoader != null) {
            return mClassLoader;
        }
        ……
        if (mIncludeCode && !mPackageName.equals("android")) {
            ……
            //创建PathClassLoader    
            mClassLoader =
                ApplicationLoaders.getDefault().getClassLoader(
                    zip, libraryPath, mBaseClassLoader);
            ……

        }
    }
}

// /frameworks/base/core/java/android/app/ApplicationLoaders.java
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{
    ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

    synchronized (mLoaders) {
        if (parent == null) {
            parent = baseParent;
        }
        if (parent == baseParent) {
            ClassLoader loader = mLoaders.get(zip);
            if (loader != null) {
                return loader;
            }
            ……
            //创建PathClassLoader
            PathClassLoader pathClassloader =
                new PathClassLoader(zip, libPath, parent);

            mLoaders.put(zip, pathClassloader);
            return pathClassloader;
        }

        PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
        ……
        return pathClassloader;
    }
}

从上面可以看出ContextImpl为用户提供了PathClassLoader来供App加载。

BaseDexClassLoader

Android中使用PathClassLoader来加载类,其实实现的就是从本地文件系统中加载类,它继承自BaseDexClassLoader,BaseDexClassLoader继承子ClassLoader,ClassLoader是一个抽象类。

// libcore/libdvm/src/main/java/java/lang/ClassLoader.java
public abstract class ClassLoader {
    
    static private class SystemClassLoader {
        //实际上为PathClassLoader
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();    
    }
    ...
    /**
     * The parent ClassLoader.
     */
    private ClassLoader parent;

    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
         //创建Android系统的ClassLoader即PathClassLoader
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }
    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }
    ……
    //返回已经被VM加载的类,如果已经加载过返回这个Class
    protected final Class<?> findLoadedClass(String className) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, className);
    }

    //class加载的逻辑,先判断是否已经加载,已经加载就直接返回,否则通过findClass进行加载
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);//查找是否已经加载过该类
        
        if (clazz == null) {//clazz为null表示未加载过
            try {
                clazz = parent.loadClass(className, false);//先通过parent加载,这也是遵循双亲委派模型
            } catch (ClassNotFoundException e) {
                // Don't want to see this.
            }

            if (clazz == null) {//parent loader未能加载则通过findClass来加载
                clazz = findClass(className);//注意findClass在该类的实现为空,它是由子类实现类加载的逻辑的
            }
        }

        return clazz;//返回加载的类
    }

    protected Class<?> findClass(String className) throws ClassNotFoundException {
        throw new ClassNotFoundException(className);
    }
}

可以看出ClassLoader为我们实现了类加载的基本逻辑,首先它通过findLoadedClass查找要加载的类是否已经加载,如果已经加载了就直接返回,否则通过parent loader进行加载,这符合双亲委派的加载模型,如果父类loader找到该类并加载则返回,否则通过子类加载器进行加载,子类加载时通过findClass进行加载的。所以需要实现findClass的具体逻辑。那么在BaseDexClassLoader我们需要重点关注findClass的类加载逻辑。

//libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
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);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
       //BaseDexClassLoader继承自ClassLoader,这里将findClass的任务委托给了DexPathList
        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;
    }
    ……
}

我们发现BaseDexClassLoader的实现非常简单,它内部有一个DexPathList成员pathList,在构造方法中进行初始化,它代表了一个jar/apk文件列表,在这些文件之中包含了class文件和资源文件。而在findClass中实际上是将加载类的任务委托给了pathList。那么就需要再取分析DexPathList了,在这之前我们看看它大概会包含的信息,这是我在应用中打印的ClassLoader信息,它内部的DexPathList包含了该apk和lib的信息。

classLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.yujian.myapplication-2.apk"],nativeLibraryDirectories=[/data/app-lib/com.yujian.myapplication-2, /system/lib]]]

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
final class DexPathList {
    ...
    private final Element[] dexElements;//Element是对应于dex/apk文件或者目录
    ……
    private final File[] nativeLibraryDirectories;


    //这里 dexPath可以包含多个dex文件,这也是为什么会叫DexPathList
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ……
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);//生成Element数组
        ……
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }
}

DexPathList有两个成员dexElements和nativeLibraryDirectories,分别用来描述dex/apk文件信息和lib文件。它们都是在DexPathList构造方法中进行初始化的。其中dexElements是通过makeDexElements和splitLibraryPath生成的。其中Element的定义如下

 static class Element {
        private final File file;//代表了源文件
        private final boolean isDirectory;//当前描述的文件是否为目录
        private final File zip;//源文件如果未压缩文件 zip和file就是同一个File
        private final DexFile dexFile;//dex文件
        ……
        public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
            this.file = file;
            this.isDirectory = isDirectory;
            this.zip = zip;
            this.dexFile = dexFile;
        }
        ……
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    for (File file : files) {//遍历文件
        File zip = null;
        DexFile dex = null;
        String name = file.getName();//文件名

        if (name.endsWith(DEX_SUFFIX)) {//后缀为.dex
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);//直接通过loadDexFile加载
            } 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)) {//后缀为.apk/.jar/.zip的情况
            zip = file;//赋值给zip

            try {
                dex = loadDexFile(file, optimizedDirectory);
            } 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 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));//添加一个Element目录 true表示为目录
        } else {
            System.logW("Unknown file type for: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));//添加一个dex文件对应的Element
        }
    }

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

makeDexElements通过dexPath代表的dex/apk文件或者目录生成对应的elements数组,在构造DexPathList时传递的dexPath时可能包含多个文件路径的,我们上面打印的信息就只有一个apk,这里需要注意,这些文件路径经过splitDexPath返回一个ArrayList代表了dexPath所代表的文件列表。通过一个循环处理这个文件列表,针对不同的文件类型比如dex/apk/jar/zip或者目录进行不同的处理:

  1. 如果时dex文件,通过loadDexFile加载,并返回一个描述该文件的DexFile,
  2. 如果时zip/apk/jar,先保存压缩文件到zip中,然后通过loadDexFile加载并返回要描述的dex文件的DexFile
  3. 如果时目录,直接为其生成一个Element并添加到elements数组中

对于1和2两种情况最终也会未其分别创建Element并添加到elements数组中,这个elements数组最终就是我们要的dexElements。

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {//对于PathClassLoader来说 optimizedDirectory总是为null的
        return new DexFile(file);//直接new一个DexFile返回 
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

loadDexFile实际上只是为file创建一个DexFile对象。从名称上看它是专门处理dex文件的。

下面我们就看看DexPathList是如何加载类的


public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {//遍历dexElements
        DexFile dex = element.dexFile;

        if (dex != null) {//通过dexFile加载类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;//加载成功
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

DexPathList先遍历dexElements,对于DexFile通过loadClassBinaryName来加载,如果找到就返回。这里又转到DexFile进行加载了,所以需要再看看DexFile是如何加载的。

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, suppressed);
}

//DexFile加载类
private static Class defineClass(String name, ClassLoader loader, int cookie,
                                    List<Throwable> suppressed) {
    Class result = null;
    try {
        //通过native来加载 runtime/native/dalvik_system_DexFile.cc
        result = defineClassNative(name, loader, cookie);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

DexFile调用defainClassNative方法来加载类,这里从名称看,它实际上是从native层加载类的,实现在runtime/native/dalvik_system_DexFile.cc。关于native层如何加载类我们在另外的篇章中进行分析。这样DexPathList加载类的逻辑就分析完成了。

需要注意的是DexPathList遍历dexElements通过DexFile来进行的类加载的方式,为一些基于muti dex的热修复技术提供了可能,因为在dexElements数组中靠前的dex文件首先被访问到,这样被修复的类可以被优先加载。

PathClassLoader

PathClassLoader是BaseDexClassLoader的子类,它的类加载功能正是依赖于其父类。 我们看看它的实现

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

PathClassLoader的实现非常简单,只是提供了两个不同的构造方法,这两个构造方法的区别在于是否提供了lib path,默认情况下的path就是system/lib/和data/app-lib/pakage-name

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.
 */
public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

DexClassLoader的实现更加简单,只有一个构造方法,从注释也可以看出它可以从包含classes.dex文件的jar/apk文件中来加载类,而不需要jar/apk为已安装应用的一部分。因为它提供了一个optimizedDirectory参数,这个参数是一个应用私有且可写入的目录,用来保存dex经过优化后的类,需要注意的是为了防止注入,优化后的类是不能被保存在外置存储上的。在PathClassLoader中我们看到这个参数默认是null,也就是它会从默认的位置加载dex,这个位置就是/data/dalvik-cache,也就是已经安装的apk。这也是它们之间最大的区别了。