Java 类加载和 Android 上类加载相关的学习笔记,大部分文字来自《深入理解 Java 虚拟机》,部分来自网上博文(我不生产知识,我只是知识的搬运工 XD)

Java 中的类加载机制

类加载流程

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)

在加载阶段,Java虚拟机需要完成以下三件事情

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

但上述过程仅仅是 Java 虚拟机的规范要求,在真正的实现上给 Java 虚拟机留下了非常大的操作空间,比如通过一个类的全限定名来获取定义此类的二进制字节流这条规则,并不限定必须从磁盘上读取 class 文件,这极大地激发了开发者的创造性:

  • 从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • 从网络中获取,这种场景最典型的应用就是Web Applet。
  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
  • 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
  • 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。
  • ……

Java 中的类加载器

  1. BootStrap ClassLoader,Java 类加载层次中最顶层的类加载器,负责加载 JDK中 的核心类库,负责加载Java的核心类库,如 rt.jar,resources.jar 等,由C++编写。
  2. Extension ClassLoader,扩展类加载器,负责加载 Java 的扩展类库,如 $JAVA_HOME/jre/lib/ext/ 目录下的所有 jar。
  3. Application ClassLoader,系统类加载器,负责加载应用程序 classpath 目录下的所有 jar 和 class 文件。
  4. 自定义类加载器,一般继承自 3,实现自定义的类加载器。

双亲委派机制

首先个人认为,将 parents delegate 翻译为 双亲委派 并不是特别准确,这个机制指的是,如果一个类加载器收到类加载的请求,它不会主动尝试自己加载类,而是将这个请求委托给父类的加载器完成,如此逐级往上,除非父加载器找不到指定类的时候,子加载器才会自己主动加载。很明显这里并没有 “双亲” 或者说两个父类的加载器的存在,译作 “双亲委派” 失去了翻译中的 “达”。

这里贴一段 Java Doc 的原文:

The Java Class Loading Mechanism

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

意义

需要注意的是,在 Java 中,当且仅当两个实例的类名、包名、以及其加载器都相同时,才会被认为同一种类型,所以当使用不同的类加载器去加载一个对象时,会被判断为不同的类型。

直接贴 深入理解 Java 虚拟机 一书的原文:

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。

ClassLoader

Android 中的类加载机制

与一般的 Java 虚拟机不同,Android 虚拟机加载的是 Dex 文件,所谓 Dalvik 虚拟机和 JVM 的区别以及二者背后的恩怨情仇按过不表,还是来看 Android 中的类加载机制。

Android 类加载器的基类是 BaseDexClassLoader,其派生出三个子类加载器:

  • BaseDexClassLoader
    • PathClassLoader
    • DexClassLoader
    • InMemoryDexClassLoader

其中 InMemoryDexClassLoader 在 Android 8.0 中被引入,在这里暂不讨论,之后开篇新文章继续分析新 ClassLoader 的意义和之前在 Android 加壳中的类似实现。

下面继续来看 Android 的类加载机制:

PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
   public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

很明显可以看到二者在本质上是对父类 BaseDexClassLoader 的继承,差别仅在于调用父类构造方法时传入的参数不同,PathClassLoader 传入的 optimizedDirectory 为 null,而 DexClassLoader 提供了optimizedDirectory(optimizedDirectory 是优化后的 odex 的存放目录),具体最终还是由父类来实现,继续来看父类的代码(这里基于 Android 6.0.1_r81):

BaseDexClassLoader

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>();
        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 实例,而之后所有的 findClass 调用都会委托给 DexPathList 来处理,继续看 DexPathList 的关键代码:

final class DexPathList {
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        // ... 省略
        // save dexPath for BaseDexClassLoader
        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);
        // ... 省略
    }
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                              List<IOException> suppressedExceptions) {
        List<Element> elements = new ArrayList<>();
        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()) {
                elements.add(new Element(file, true, null, null));
            } else if (file.isFile()) {
                if (name.endsWith(DEX_SUFFIX)) {
                    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(dir, false, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }
    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 对 loadDexFile 有较大的影响,继续跟进 DexFile 查看其代码:

public final class DexFile {    
    /**
     * Opens a DEX file from a given filename. This will usually be a ZIP/JAR
     * file with a "classes.dex" inside.
     *
     * The VM will generate the name of the corresponding file in
     * /data/dalvik-cache and open it, possibly creating or updating
     * it first if system permissions allow.  Don't pass in the name of
     * a file in /data/dalvik-cache, as the named file is expected to be
     * in its original (pre-dexopt) state.
     *
     * @param fileName
     *            the filename of the DEX file
     *
     * @throws IOException
     *             if an I/O error occurs, such as the file not being found or
     *             access rights missing for opening it
     */
    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) {
            }
        }

        mCookie = openDexFile(sourceName, outputName, flags);
        mFileName = sourceName;
        guard.open("close");
    }
    // ... 省略
    static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {
        return new DexFile(sourcePathName, outputPathName, flags);
    }
}

很明显从注释里可以看到,当 optimizedDirectory 为 null 的情况下,调用 new DexFile(file),odex 只能保存到 /data/dalvik-cache 目录下,而 DexFile.loadDex(file.getPath(), optimizedPath, 0) 能将 odex 保存到自定义的目录下。

总结

在 Android 同样实现了双亲委派机制的类加载器,分别为 PathClassLoaderDexClassLoader ,均继承自父类 BaseDexClassLoader,其中 DexClassLoader 在 Android 插件化、动态代码加载等领域有较大的用途。

需要看详细调用流程的,可以继续参考该篇文章:谈谈 Android 中的 PathClassLoader 和 DexClassLoader

End

上述笔记参考诸多资料,来源较多恕不一一列举,如有使用不当之处,还望不吝指出。