Java类加载器及Android类加载器基础

Posted aspook

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java类加载器及Android类加载器基础相关的知识,希望对你有一定的参考价值。

引子

android插件化与热更新技术日渐成熟,当你研究这些技术时会发现类加载器在其中占据重要地位。Java语言天生就有灵活性、动态性,支持运行期间动态组装程序,而这一切的基础就是类加载器。

Java中的类加载器

Java灵活性和动态性的原因

Java源代码被编译器编译成字节码,即从.java文件编译为.class文件,而.class文件就是通过类加载器加载到虚拟机内存中的。

虚拟机的类加载(Class Loading)过程分为加载、链接(验证、准备、解析)、初始化、使用、卸载等过程。这里仅考虑加载这个阶段,在此阶段虚拟机的工作有以下几点:

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

注意看第1条,虚拟机规范只是说来获取一个类的二进制字节流,但并没有说从哪里获取,怎样获取,这也就意味着Class文件可以来自磁盘、ZIP文件、JAR文件、数据库、甚至来自网络或者在程序运行时动态生成。上述各种来源的Class文件都是由类加载器(Class Loader)来加载的,也正因为如此,Java才拥有高度的灵活性和动态性。

Java中的几种类加载器

Java中的类加载器至少有三种:

  • 启动类加载器——该加载器一般由C或C++实现(如HotSpot用C++实现,其实也有虚拟机是用Java实现的),它是作为虚拟机不可分割的一部分而存在。该加载器负责加载jre/lib中的系统类,如通常从rt.jar中进行加载。启动类加载器由于属于虚拟机的一部分,因此无法被Java程序直接引用,所以例如String.class.getClassLoader()将会返回null。
  • 扩展类加载器——该加载器由Java语言实现,继承自java.lang.ClassLoader,独立于虚拟机外部,负责加载jre/lib/ext目录下的文件,如果对扩展类加载器调用getParent()也会返回null。
  • 系统类加载器(或叫应用类加载器)——该加载器由Java语言实现,继承自java.lang.ClassLoader,独立于虚拟机外部,负责加载应用程序类。如果应用程序中没有自定义的类加载器,那么此加载器就是默认的类加载器。

此外,用户还可以继承ClassLoader类来自定义类加载器,这样就可以在向虚拟机传递字节码之前进行需求定制了。

注意:对于任意一个Java类,它在虚拟机里的唯一性是由其类本身及其类加载器共同决定的。如果两个类来自同一个Class文件,在同一个虚拟机中,但是被不同的ClassLoader所加载,那么这两个类在虚拟机中也是不相等的。

类加载器的双亲委派模型

先来看下Java中的类加载器层次关系:

这里写图片描述

上述层次关系称为类加载器的双亲委派模型,它是在JDK 1.2中引入的,其实它并非强制性的约束,而是推荐我们使用的一种类加载机制,可以看到除了顶部的启动类加载器之外,其他加载器都有一个父类加载器。

双亲委派模型的工作流程:当一个类加载器收到加载类的请求时,它自己先不进行加载,而是把该请求委派为父类加载器去完成,父类加载器也是如此,直到将加载类的需求传给顶层的启动类加载器;只有当父类加载器无法完成加载时(在自己的搜索范围中没有找到该类),子加载器才尝试自己去完成类加载,如果加载不了,则会抛出ClassNotFoundException异常。

有一点需要注意:如果扩展类加载器收到请求去加载一个类,它会先委托启动类加载器去加载,如果启动类加载器加载不了,则尝试自己加载。如果扩展类加载器也无法加载,则直接抛出ClassNotFoundException异常而结束,并不会再交给下一层的应用类加载器去加载。

说明了双亲委派模型的原理后,再来看下其源码实现,代码逻辑很简单,也证实了上述讲到的双亲委派模型的工作流程:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    // 首先判断该类是否已经被加载过,如果已加载过就直接返回
    Class c = findLoadedClass(name);
    if (c == null) {
        // 如果没有被加载,就委托给父加载器处理或者给启动类加载器处理
        try {
            if (parent != null) {
                // 如果存在父类加载器,就委派给父类加载器加载  
                c = parent.loadClass(name, false);
            } else {
                // 如果不存在父类加载器,就检查是否由启动类加载器加载  
                // 通过调用native方法 findBootstrapClass0(String name)  
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父加载器和启动类加载器都不能完成加载任务,自身才尝试去加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

双亲委派模型的意义

使用双亲委派模型来组织各种类加载器,使之遵循了一定的优先级层次,从而能保证Java运行环境的稳定与条理性。例如java.lang.Object类是所有类的基类,并且根据双亲委派模型它是由启动类加载器加载的,如果我们也自定义了一个java.lang.Object类(只是假如,其实虚拟机会对java.lang开头的自定义类抛异常)并放在应用程序的ClassPath中去加载,那么应用中就会出现多个Object类,从而会导致Java类型体系混乱而无法正常运行。

另一个好处是避免类的二次加载。从上述loadClass源码中可知,先判断该类是否被加载过,如果已被加载过则直接返回该类。当一个类加载器委托父类加载时也是执行此逻辑,从而保证某些类只被加载一次。

自定义类加载器

由于自定义类加载器通常继承ClassLoader,来看下ClassLoader的几个主要方法:

// 加载指定完整名称的二进制字节流,不建议子类加载器重写,否则可能会破坏双亲委派模型
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }

// 加载指定完整名称的二进制字节流,不建议子类加载器重写,否则可能会破坏双亲委派模型
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }

// 被loadClass方法调用去加载指定名称类,官方建议子类加载器重写该方法
protected Class<?> findClass(String name) throws ClassNotFoundException { … }

// 该方法将二进制字节流转换为Class,一般在findClass方法中读取到对应字节码后调用,由于是final方法,故不可继承,其功能具体由虚拟机实现,Java层不需要关心
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }

上述几个方法的说明可参考注释。

为了遵循双亲委派模型,当自定义类加载器时,官方建议我们仅仅重写findClass()方法,而不要重写loadClass()方法,否则就有可能破坏双亲委派模型。当然前面也说了,双亲委派模型并非强制约束,如有特别需要,也可以自行确定类的加载规则。一个典型的自定义类加载器如下:

public class CustomClassLoader extends ClassLoader {
    ……
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
   ……

}

由此可知类加载器的各个方法的执行顺序为:loadClass—>findClass—>defineClass。

Android中的类加载器

Android应用通常是使用Java来开发的,也是运行在虚拟机Dalvik或ART上。虽然Android的虚拟机跟标准的Java虚拟机是不同的,但是类的加载机制都是类似的,即理论上Android也可以像Java程序一样,灵活地动态加载,如今大量的Android插件化、热更新框架都利用了此技术。

Android中的几种类加载器

在一个Android工程的Application中加入几行日志来打印下,代码如下:

package com.aspook.androidnotes;

import android.app.Application;
import android.util.Log;

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ClassLoader loader = getClassLoader();
        if (loader != null) {
            Log.d("ABC", "classLoader :" + loader);
            while (loader.getParent() != null) {
                loader = loader.getParent();
                Log.d("ABC", "classLoader :" + loader);
            }
        }
    }
}

在Android Studio中启动App后,依次输出3条Log如下:

classLoader :dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/com.aspook.androidnotes-2/base.apk”],nativeLibraryDirectories=[/data/app/com.aspook.androidnotes-2/lib/arm64, /vendor/lib64, /system/lib64]]]
classLoader :com.android.tools.fd.runtime.IncrementalClassLoader@3faf711
classLoader :java.lang.BootClassLoader@d913983

这里出现了3种ClassLoader,分别是:dalvik.system.PathClassLoader、com.android.tools.fd.runtime.IncrementalClassLoader、java.lang.BootClassLoader。第二个类加载器是用于Instant Run的,如果关闭Android Studio的Instant Run功能,再运行App则只会输出两种ClassLoader。

通过查看dalvik.system包下的源码,发现还有一种ClassLoader叫做DexClassLoader,稍后会介绍其用途。

PathClassLoader

其官方说明如下:

Provides a simple ClassLoader implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).

PathClassLoader是ClassLoader的简单实现且只能加载本地的列表文件或目录,在Android中也就是已安装好的APK,它不能加载来自网络的类。Android中的系统类加载器与应用类加载器都是PathClassLoader。

先来看其源码(7.0):

package dalvik.system;

import dalvik.system.BaseDexClassLoader;
import java.io.File;

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

从上述源码可知其仅仅提供了两个构造方法,其中各参数的具体含义如下:

dexPath:包含dex文件的JAR/ZIP/APK文件的路径

librarySearchPath:native library文件的路径

parent:父类加载器

再来看BaseDexClassLoader的源码:

package dalvik.system;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, 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;
    }

    /**
     * * @hide
     */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }

    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }

    @Override
    protected Enumeration<URL> findResources(String name) {
        return pathList.findResources(name);
    }

    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

    @Override
    protected synchronized Package getPackage(String name) {
        if (name != null && !name.isEmpty()) {
            Package pack = super.getPackage(name);

            if (pack == null) {
                pack = definePackage(name, "Unknown", "0.0", "Unknown",
                        "Unknown", "0.0", "Unknown", null);
            }

            return pack;
        }

        return null;
    }

    /**
     * @hide
     */
    public String getLdLibraryPath() {
        StringBuilder result = new StringBuilder();
        for (File directory : pathList.getNativeLibraryDirectories()) {
            if (result.length() > 0) {
                result.append(':');
            }
            result.append(directory);
        }

        return result.toString();
    }

    @Override
    public String toString() {
        return getClass().getName() + "[" + pathList + "]";
    }
}

BaseDexClassLoader构造方法中有一个新的参数为optimizedDirectory,它表示优化后的dex文件要写入的路径,此处可以为null。

BaseDexClassLoader继承自java.lang.ClassLoader,它跟纯Java环境下的java.lang.ClassLoader还是有些不同的,虽然双亲委派的加载机制类似。

结合最初的Log输出可知,PathClassLoader只能加载”/data/app/com.aspook.androidnotes-2/base.apk”中的类,也就是已安装到手机中的APK,因此PathClassLoader作为默认的应用类加载器。

DexClassLoader

其官方说明如下:

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.

DexClassLoader可以从包含dex文件的JAR或APK中来加载类,而这些代码源允许不必是安装应用的一部分,因此可用于动态加载。

先来看下DexClassLoader的源码(7.0):

package dalvik.system;

/**
 * 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.getCodeCacheDir()} to create
 * such a directory: <pre>   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }</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.
 */
import dalvik.system.BaseDexClassLoader;

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     * <p>
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath            the list of jar/apk files containing classes and
     *                           resources, delimited by {@code File.pathSeparator}, which
     *                           defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     *                           should be written; must not be {@code null}
     * @param librarySearchPath  the list of directories containing native
     *                           libraries, delimited by {@code File.pathSeparator}; may be
     *                           {@code null}
     * @param parent             the parent class loader
     */


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

    }

}

它同样继承自BaseDexClassLoader,是java.lang.ClassLoader的子类,因此DexClassLoader与PathClassLoader都默认遵循双亲委派模型。

DexClassLoader构造方法中的参数,我们前文都已经提及,注意的一点是optimizedDirectory参数在这里不能为null。

与PathClassLoader不同,DexClassLoader则打破了PathClassLoader的局限,它可以加载已安装应用之外的APK、JAR或ZIP中的dex文件,通常建议使用如下路径:

File dexOutputDir = context.getCodeCacheDir();

不建议使用外部存储,因为外部存储没有提供足够的访问权限控制,容易引发代码注入攻击。

因此,Android中实现动态插件通常是自定义继承自DexClassLoader的类加载器;如果插件为已安装的APK,则可以使用PathClassLoader。

BootClassLoader

BootClassLoader直接继承自java.lang.ClassLoader,其定义如下:

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

    @Override
    protected URL findResource(String name) {
        return VMClassLoader.getResource(name);
    }

    @SuppressWarnings("unused")
    @Override
    protected Enumeration<URL> findResources(String resName) throws IOException {
        return Collections.enumeration(VMClassLoader.getResources(resName));
    }

    @Override
    protected Package getPackage(String name) {
        if (name != null && !name.isEmpty()) {
            synchronized (this) {
                Package pack = super.getPackage(name);

                if (pack == null) {
                    pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0",
                            "Unknown", null);
                }

                return pack;
            }
        }

        return null;
    }

    @Override
    public URL getResource(String resName) {
        return findResource(resName);
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }

    @Override
    public Enumeration<URL> getResources(String resName) throws IOException {
        return findResources(resName);
    }
}

通常在自定义类加载器时,都需要在构造方法中传入一个父加载器,而BootClassLoader的构造方法如下,没有传入parent,而是传入一个null:

public BootClassLoader() {
    super(null);
}

因此调用BootClassLoader的getParent方法时返回值为null。

BootClassLoader用来加载系统框架级别的类,例如Context.class.getClassLoader()与ListView.class.getClassLoader()的返回值类型均为BootClassLoader。

系统类加载器

当调用ClassLoader.getSystemClassLoader()这句代码时,会输出如下结果:

dalvik.system.PathClassLoader[DexPathList[[directory “.”],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]

发现系统类加载器也是dalvik.system.PathClassLoader,与最初应用的类加载器(也是dalvik.system.PathClassLoader)不同的是DexPathList的路径不同。

跟踪一下源码:

public static ClassLoader getSystemClassLoader() {
    return SystemClassLoader.loader;
}
static private class SystemClassLoader {
    public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
/**
 * Encapsulates the set of parallel capable loader types.
 */
private static ClassLoader createSystemClassLoader() {
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");

    // String[] paths = classPath.split(":");
    // URL[] urls = new URL[paths.length];
    // for (int i = 0; i < paths.length; i++) {
    // try {
    // urls[i] = new URL("file://" + paths[i]);
    // }
    // catch (Exception ex) {
    // ex.printStackTrace();
    // }
    // }
    //
    // return new java.net.URLClassLoader(urls, null);

    // TODO Make this a java.net.URLClassLoader once we have those?
    return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

而System.getProperty(“java.class.path”)返回值为“.”,似乎可以解释系统类加载器的DexPathList的路径了。

Android中类加载器的层次结构

与Java中类加载器的层次结构类似,具体如下图:

这里写图片描述

总结

本文简单介绍了类加载器的基本概念,罗列了Java及Android中常用的类加载器,并对各种类加载器的特点及功能做了说明,另外对类加载器的双亲委派机制做了详细讲解,对于Android插件化及热更新技术则不在本文的讨论之内,后续会继续分享。

以上是关于Java类加载器及Android类加载器基础的主要内容,如果未能解决你的问题,请参考以下文章

类加载器及双亲委派模型

Java 类加载器及加载Class字节码

用户自定义类加载器及具体实现

用户自定义类加载器及具体实现

java面向对象--类加载器及反射

Java中常见的类加载器及双亲委派机制的原理