安卓虚拟机系列二:深入分析ClassLoader 机制

1,072 阅读6分钟

Android 中各种ClassLoader的作用

本文概述:

  • 此为android 虚拟机系列第二篇文章,文章重点探究了android 平台中的类加载器,涵盖类加载器作用,常见的类加载器,重要类加载器工作流程,双亲委派机制,对于部分重难点深入源码进行了细致剖析

大体描述:

  • ClassLoader 作用:加载类,联通真实的类文件与内存中使用的类

  • 安卓中有哪些ClassLoader:查看继承关系

    • 补充知识:Class

      • 在Class 中有一个成员属性 classLoader

         private transient ClassLoader classLoader;
        
      • 使用 .getClassLoader() 方法即可查看这个对象对应的类是由哪一个类加载器加载的

    • ClassLoader:继承关系

      • SecureClassLoader:安卓中用不到这个

      • BaseClassLoader

        • PathClassLoader
        • ImMemoryDexClassLoader:android 8.0后出现的
        • DexClassLoader
      • BootClassLoader:用来加载android FrameWork层的类

        • android FrameWork层的类(手机内置的):String,Activity

          • 使用 .getClassLoader() 方法即可查看这个对象对应的类是由哪一个类加载器加载的
        • AppCompatActivity 就不是,这个只是Google的一个官方类

          • 由PathClassLoader 加载
  • PathClassLoader :安卓程序的类加载器

    • 从上下文中调用getClassLoader,得到的是整个程序的类加载器(PathClassLoader)
    • 这个跟BootClassLoader的区别

      • BC:加载的是系统层的东西,PC加载的是用户层的东西
    • 安卓中的类加载器针对于android 平台构建,但顶层的抽象类(ClassLoader ),大家都是一样的

    • Java 中加载 .class 流程

      • 读取真实的 .class 文件,按照一定格式,解析成 JVM中的Class对象,然后使用
      • 耗时 ---> 缓存机制
    • 安卓中加载 .dex

      • 解析成Class,然后使用
    • 工作流程:PathClassLoader

      • 需要使用这个类(实例化),通过程序的ClassLoader(PathClassLoader),调用loadClass方法

ClassLoader 加载流程与双亲委派机制

怎么去加载一个类?

 //返回一个Class 对象
 Class classObject = getClassLoader().loadClass("需要加载类的全类名")

PathClassLoader.loadClass() 在哪里?

  • 这个里面是没有 loadClass方法的:查看其父类BaseDexClassLoader

    image-20220702204051860

  • BaseDexClassLoader 里面没有:再进入父类ClassLoader

    image-20220702204112368

  • 这个里面就有了:有两个,有一个重载,查看关键的哪一个

    image-20220702204307091

源码分析:LoadClass

  • 签名:传入全类名,加载一个类

     protected Class<?> loadClass(String name, boolean resolve)
    
  • 寻找缓存:之前加载了的就不加载了,节省时间,缓存命中(return),没有命中进入if

     Class<?> c = findLoadedClass(name);
    
    • Java 中会 I/O(读取真实的 .class文件,耗时),然后解析

    • 最终会调用到一个native方法:JIN层的C/C++

       native static Class findLoadedClass(ClassLoader c1,String name);
      
  • 进入 if :

    • 拿到当前对象的父类加载器: BootClassLoader 不是这个类(PathCladdLoader)的父类(BaseClassLoader)

       c = parent.loadClass(name, false);
      
      • 抽象类ClassLoader 存在类属性parent

         private final ClassLoader parent;
        
    • 执行流程:向上加载,双亲委派机制

      • 假设当前对象的父类加载器,还有父类加载器,就一直向上调用

      • 这里的parent 是ClassLoader的一个属性,并不是这个类的父类

        • 比如,创建一个PathClassLoader

           //classLoader:传入BootClassLoader(由系统调用的),赋值给PathClassLoader 的类属性 parent
           new PathClassLoader(String string,ClassLoader classLoader);
          
        • 为什么要设置成BootClassLoader

双亲委派机制

  • 工作流程:先让父类加载器去加载,找不到自己再去找

  • 细节:

    • 避免重复:

      • 如果类A已经被类加载器B加载了,那么从B 的缓存中去找 A的信息,这样就不用自己再去找一次,
      • 如果说又加载了一次,引发了异常
    • 数据结构:map<k,v>

      • K:类加载器
      • V:类的信息
    • 加载问题:

      • PathClassLoader 是由BootClassLoader加载
      • BootClassLoader:Linux层加载的
    • 整体流程:责任链设计模式

      • 当类属性 parent存在,交给下面的去执行

      • 补充:责任链设计模式的应用

        • OkHttp 的五大拦截器、View的绘制流程、事件处理与分发
  • 安全性考虑:

    • 没有双亲委派机制:意味着不存在缓存

       if (parent != null) {
           c = parent.loadClass(name, false);
       }
      
      • 当使用String类,找到的是用户自定义的,虽然包名相同但逻辑不同 ---> 相当于破坏系统源码了;

         //用户自定义的代码
         package java.lang;
         public class String{
             ……
         }
        
    • 当存在双亲委派机制:存在缓存

      • String类的寻找,先从BootClassLoader找,发现有,那么直接用,不去加载用户自定义的String类;

那么用户自定义的类:例如MainActivity这些去哪里找

  • 执行流程:

    • 使用PathClassLoader 寻找程序的类(自定义的);但PathClassLoader 没有findClass 方法,进入其父类加载器BaseDexClassLoader ,通过里面的findClass 方法去找;
    • BaseDexClassLoader.findClass 去调用DexPathList.findClass,因为DexPathList 存在element数组,其中的每一个元素相当于一个dex,此时去遍历这个数组,拿到dex之后去调用dexFile.loadClassBinaryName 去找
    • 最终是调用到native方法:
     private static native Class defineClassNative(String name,ClassLoader loader,Object cookie) throws ClassLoaderNotFoundException,NoClassDeffFoundError;
    
  • 图示:

    image-20220703104239858

  • 示意图:

    image-20220703102902707

  • 具体分析过程:都是使用PathClassLoader加载,

     //创建时会传入dex/Apk的地址:就是当前程序对应的文件
     public PathClassLoader(String dexPath, ClassLoader parent) {
         super(dexPath, null, null, parent);
     }
    
  • 此时应该调用LoadClass 中自己找的部分

     if (c == null) {
         // If still not found, then invoke findClass in order
         // to find the class.
         c = findClass(name);
     }
    
    • 调用 PathClassLoader,看其中有无findClass方法,没有,那么进入其父类BaseDexClassLoader,发现有
  • BaseDexClassLoader.findClass

    • 执行流程:

      • 查看共享文件中有没有
       //这是一个:protected final ClassLoader[] sharedLibraryLoaders;
       在BaseDexClassLoader构造时初始化
           public BaseDexClassLoader(String dexPath,
                   String librarySearchPath, ClassLoader parent, ClassLoader[] libraries)
       ​
       if (sharedLibraryLoaders != null) {
           ……
      
      • 找类
       Class c = pathList.findClass(name, suppressedExceptions);
      
      • 此时调用DexPathList.findClass 去遍历element数组

         //去拿到每一个element,再拿到里面的dexFile,就是dex文件了
         for (Element element : dexElements) {
        
        • 拿具体的dex 文件,找不到就返回null,后面就抛出异常了

           Class<?> clazz = element.findClass(name, definingContext, suppressed);
          

问题:

  • dex 文件是哪里来的?

    • APK里面的
  • Element 数组中有多少个元素

    • Apk 中有多少个dex文件,那么就有多少个元素
  • ClassLoader 是怎么跟文件关联的:通过传参(将dex 的地址传进来)

 public PathClassLoader(String dexPath, ClassLoader parent) {
     super(dexPath, null, null, parent);
 }
  • 操作与对应的配置文件:

    • 操作jar 包:jarFile
    • 操作dex 文件:dexFile
  • PathList 是什么
 private final DexPathList pathList;
  • 在BaseDexClassLoader 构造中实例化

    • 传入需要加载的应用(jar)的dex/apk地址
     public BaseDexClassLoader(String dexPath,
             String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
             boolean isTrusted) {
         super(parent);
         
         this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
     }
  • DexPathList 干了什么?

    • 判空,抛出空指针异常
     if (definingContext == null) {
         throw new NullPointerException("definingContext == null");
     }
    
    • 根据dexPath:传入应用对应的APK文件
     this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                        suppressedExceptions, definingContext, isTrusted);
    
    • 最终封装成element数组:
     //一个dex,一个element对象,可能存在多个,那就搞一个数组
     private Element[] dexElements;
    
    • PathClassLoader 为什么可以处理多个dex文件:splitDexPath 的作用:进行分组、拆组

      • 比如目录A里面a.dex 与 b.dex,那么就传入下面这样的字符串

         /a/a.dex:/a/d.dex
        
      • 根据 : 分割出一个List 集合,一个冒号,集合中两个item属性

    • findClass原代码

     protected Class<?> findClass(String name) throws ClassNotFoundException {
         // First, check whether the class is present in our shared libraries.
         if (sharedLibraryLoaders != null) {
             for (ClassLoader loader : sharedLibraryLoaders) {
                 try {
                     return loader.loadClass(name);
                 } catch (ClassNotFoundException ignored) {
                 }
             }
         }
         // Check whether the class in question is present in the dexPath that
         // this classloader operates on.
         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;
     }