Android ClassLoader 详解

Java中的 ClassLoader

铺垫

ClassLoader就是类加载器。类加载子系统的主要作用是通过多种类加载器来查找和加载Class文件到Java虚拟机当中。

类别

  1. java中的类加载器有两种类型:系统类加载器和自定义类加载器。

    系统类加载器有三种,分别是Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。

  2. Bootstrap ClassLoader(引导类加载器)

    用于加载指定的JDK的核心类库,比如说java.lang.、java.uti. 这些系统类。Java虚拟机的启动就是通过Bpptstrap ClassLoader创建一个初始类来完成的。

    它用来加载以下目录中的类库:

    • $JAVA_HOME/jre/lib目录。
    • -Xbootclasspath参数指定的目录。

    特点:

    • 使用C/C++语言实现,不能被Java代码访问到

    • 并不是继承自java.lang.ClassLoader

    可以通过打印System.getProperty("sun.boot.class.path")来获取引导类加载器所加载的目录,可以发现基本上都是$JAVA_HOME/jre/lib目录中的jar包。

  3. Extensions ClassLoader(拓展类加载器)

    可以简称为ExtClassLoader,用于加载Java中的拓展类,提供除了系统类之外的额外功能。

    ExtClassLoader用来加载以下目录中的类库:

    • 加载$JAVA_HOME/jre/lib/ext目录。
    • 系统属性java.ext.dir所指定的目录。
  4. Application ClassLoader(应用程序类加载器)

    可以简称为AppClassLoader,同时也可以称作System ClassLoader(系统类加载器),这是因为AppClassLoader可以通过ClassLoader的getSystemClassLoader方法获取到。它用来加载以下目录中的类库:

    • 当前程序的ClassPath目录
    • 系统属性java.class.path指定的目录
  5. Custom ClassLoader(自定义类加载器)

    通过继承java.lang.ClassLoader类的方式来实现自己的类加载器。Extensions ClassLoader和App ClassLoader也是继承自java.lang.ClassLoader类。

继承关系

ClassLoader的父子关系并不是使用继承来实现的,而是使用组合来实现代码复用的。

ClassLoader的加载过程

双亲委托模式

类加载器的流程是首先查找class文件,然后再把文件加载到虚拟机中;双亲委托模式是首先判断该class是否已经加载,如果没有加载,那么不是自身去查找,而是委托给父加载器进行查找,也就是按照custom-app(application/system)-ext(extensions)-bootstrap的顺序依次向上委托,委托给最顶层的Bootstrap之后,如果它找到了该class,就会直接返回,如果没找到,则继续依次向下查找;如果还没找到则最后会交由自身去查找。

  1. 为什么要使用双亲委托这种模型呢?

    • 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是直接读取已经加载的Class。
    • 更加安全,如果不使用这种委托模式,那我们就可以随时使用自定义的String类来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
  2. JVM在搜索类的时候,是如何判定两个class是相同的呢?
        JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。

Android中的ClassLoader

区别

  • java中的ClassLoader本质上是加载Class文件,但是这一点在Android中并非如此,因为无论是DVM还是ART,它们加载的都不再是Class文件,而是dex文件,这就需要重新设计ClassLoader相关类。
  • dex文件:Android程序一般是使用Java语言开发的,但是Dalvik虚拟机并不支持直接执行JAVA字节码,所以会对编译生成的.class文件进行翻译、重构、解释、压缩等处理,这个处理过程是由dx进行处理,处理完成后生成的产物会以.dex结尾,称为Dex文件。

类别

  • 系统类加载器
    • BootClassLoader
    • PathClassLoader
    • DexClassLoader
  • 自定义加载器
1. BootClassLoader
class BootClassLoader extends ClassLoader {
    private static BootClassLoader instance;

    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null, true);
    } }

Android系统启动的时候会使用BootClassLoader来预加载常用类,但是它和SDK中的Bootstrap ClassLoader不同,它并不是由C/C++代码实现的,而是由java实现的,BootClassLoader是ClassLoader的内部类,并继承自ClassLoader。BootClassLoader是一个单例类。

2. DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {

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

可以加载dex文件以及包含dex的压缩文件(apk和jar文件),不管加载那种,最终都要加载dex文件。

DexClassLoader只是简单的封装了BaseDexClassLoader对象,没有复写父类的任何方法。

其构造方法包括4个参数:

  • dexPath: dex相关文件路径集合,多个路径用文件分隔符分隔,默认文件分隔符为“:”。
  • optimizedDirectory: 解压的dex文件存储路径
  • 包含C/C++库的路径集合
  • 父加载器
3. PathClassLoader
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);
    }
}

用来加载系统类和应用程序的类;

其构造方法中没有参数optimizedDirectory,这是因为PathClassLoader已经默认了这个参数的值是当前应用程序的私有路径、内部存储路径:/data/dalvik-cache, 所以它通常是用来加载已经安装的apk的dex文件的(安装的apk的dex文件会存储在/data/dalvik-cache中。

  • dexPath : 包含 dex 的 jar 文件或 apk 文件的路径集,多个以文件分隔符分隔,默认是“:”
  • libraryPath : 包含 C/C++ 库的路径集,多个同样以文件分隔符分隔,可以为空
4. ClassLoader
public abstract class ClassLoader {
    private ClassLoader parent;  //记录父类加载器

    protected ClassLoader() {
        this(getSystemClassLoader(), false);
    }

    protected ClassLoader(ClassLoader parentLoader) {
        this(parentLoader, false);
    }

    ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
        if (parentLoader == null && !nullAllowed) {
            //父类的类加载器为空,则抛出异常
            throw new NullPointerException("parentLoader == null && !nullAllowed");
        }
        parent = parentLoader;
    }
    static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }

    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }

    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
}

继承关系

img
  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类,在Android系统启动的时候用于预加载常用类。
  • SecureClassLoader类和JDK8中的同名类的代码是一样的,它继承自抽象类ClassLoader。但是它并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader类和JDK8中的同名类的代码是一样的,他继承自Secure ClassLoader,用来通过URL路径从jar文件和文件夹中加载类和资源。
  • InMemoryDexClassLoader是Android 8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。
  • BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader、DexClassLoader和InMemoryDexClassLoader都继承自它。

ClassLoader的加载过程

Android的ClassLoader同样遵循了双亲委托模式,抽象类ClassLoader中定义了ClassLoader的加载方法loadClass,如下所示:

public abstract class ClassLoader {

    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        //判断当前类加载器是否已经加载过指定类,若已加载则直接返回
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) { 
            try{
                if(parent!=null){
                    //如果没有加载过,则调用parent的类加载递归加载该类,若已加载则直接返回
            clazz = parent.loadClass(className, false);
                }
                else{
                  clazz=findBootstrapClassOrNull(name);
                }
                
            }catch(ClassNotFoundException e){}
            
            
            if (clazz == null) {
                //还没加载,则调用当前类加载器来加载
                clazz = findClass(className);
            }
        }
        return clazz;
    }
}

大致流程是首先通过findLoadedClass() 判断当前类加载器是否已经加载过指定类,如果已经加载就返回该类,如果没有加载就判断有没有父加载器,然后调用父加载器的loadClass方法,如果不存在父加载器就调用findBootstrapClassOrNull方法,这个方法会直接返回null。如果最后还是没找到,那么说明向上委托流程没有检查出类已经被加载,会调用当前的类加载器来进行加载。

总结一下:

  1. 判断当前类加载器是否已经加载过指定类,若已加载则直接返回,否则继续执行。
  2. 调用parent的类加载递归加载该类,检测是否加载,若已加载则直接返回,否则继续执行。
  3. 调用当前类加载器,通过findClass加载。

findClass方法如下所示:

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

在findClass方法中直接抛出了异常,这说明findClass方法需要子类来实现,BaseDexClassLoader(是ClassLoader的具体实现类)的代码如下所示:

...
 public BaseDexClassLoader(String dexPath,String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        if (reporter != null) {
            reportClassLoaderChain();
        }
    }
...
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
  List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions); //1
        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,在注释1处调用了DexPathList的findClass方法:

public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//1
            Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
      suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

在注释1处遍历Element数组dexElements,在注释2处调用Elements的findClass方法,其中Element是DexPathList的静态内部类:

 /*package*/ static class Element {
        private final File path;
        private final Boolean pathIsDirectory;
        private final DexFile dexFile;
        private ClassPathURLStreamHandler urlHandler;
        private boolean initialized;
        public Element(DexFile dexFile, File dexZipPath) {
            this.dexFile = dexFile;
            this.path = dexZipPath;
        }
        public Element(DexFile dexFile) {
            this(dexFile, null);
        }

        public Element(File path) {
            this(null, path);
        }
     ...
public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
   return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;//1
        }

从Element的构造方法可以看出,其内部封装了DexFile,它用于加载dex。

在注释1处如果DexFile不为空,就调用DexFile的loadClassBinaryName方法:

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

在loadClassBinaryName方法中调用了DexFile的defineClass方法:

    private static Class defineClass(String name, ClassLoader loader, Object cookie,DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);//1
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

在注释1处又调用了defineClassNative方法来记载dex相关文件,这个方法是Native方法。

static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
                                        jobject cookie) {
  std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
  if (dex_files.get() == nullptr) {
    return nullptr; //dex文件为空, 则直接返回
  }

  ScopedUtfChars class_name(env, javaName);
  if (class_name.c_str() == nullptr) {
    return nullptr; //类名为空, 则直接返回
  }

  const std::string descriptor(DotToDescriptor(class_name.c_str()));
  const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str())); //将类名转换为hash码
  for (auto& dex_file : *dex_files) {
    const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
    if (dex_class_def != nullptr) {
      ScopedObjectAccess soa(env);
      ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
      class_linker->RegisterDexFile(*dex_file);
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(
          hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
      //获取目标类
      mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,
                                                        class_loader, *dex_file, *dex_class_def);
      if (result != nullptr) {
        // 找到目标对象
        return soa.AddLocalReference<jclass>(result);
      }
    }
  }
  return nullptr; //没有找到目标类
}

其主要内容是创建目标类的对象并添加到虚拟机列表。

总结:ClassLoader的加载过程就是遵循着双亲委托模式,如果委托流程没有检查到此前加载过传入的类,就调用当前类加载器的findClass方法,Java层最终会调用DexFile的defineClassNative方法来执行查找流程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容