Android_类加载机制之双亲委派

本文目标

深入理解Android的类加载机制

  • 1.什么是双亲委派
  • 2.双亲委派下的Class文件加载流程
  • 3.Android中的类加载器
  • 4.PathClassLoaderDexClassLoader 到底有何不同
  • 5.Class文件加载步骤和源码分析
  • 6.Class.forNameClassLoader.loadClass加载有何不同

1.什么是双亲委派

  • 1.加载.class文件的时候,以递归的的形式逐级向上委托给父加载器ParentClassLoader去加载,如果加载过了,就不用在加载一遍
  • 2.如果父加载器也没加载过,则继续委托给父加载器去加载,一直到这条链路的顶级,顶级classLoader判断如果没加载过,则尝试加载,加载失败,则逐级向下交还调用者来加载.
双亲委派是如何实现的

任意一个classLoader对象都会有一个parent对象,我们下面的customClassLoader创建的时候虽然没有传递parent对象,但是在下面的ClassLoader类中的空参构造方法可以看出,会调用getSystemClassLoader()从而调用ClassLoader.createSystemClassLoader();,最后创建了一个PathClassLoader对象作为parent,而且在创建PathClassLoader的同时也指定了它的parentBootClassLoader

 ClassLoader customClassLoader= new ClassLoader() {
            @Override
            public Class loadClass(String name) throws ClassNotFoundException {
                return super.loadClass(name);
            }
  };
public abstract class ClassLoader {

    static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }

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

    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

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

    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        //最终会调用PathClassLoader这个classLoader
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }
    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
            //1.先检查是否已经加载过--findLoaded
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    //2.如果自己没加载过,存在父类,则委托父类
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //3.如果父类也没加载过,则尝试本级classLoader加载
                    c = findClass(name);
                }
            }
           return c;
    }
}

来继续看是如何实现的,看下loadClass方法,对于任意一个classLoader对象来说,来加载文件的时候都会调用loadClass方法

  • 1.先检查自己是否已经加载过class文件了--用这个findLoadedClass方法,如果已经加载了直接返回就好了
  • 2.如果自己没加载过,存在父类,则委托父类去加载--用这个parent.loadClass(name, false)方法,此时就会向上传递,然后就会去父加载器中循环第1步,一直到顶级ClassLoader
  • 3.如果父类也没加载过,则尝试本级classLoader加载,如果加载失败了就会向下传递,交给调用方来实现.class文件的加载

2.双亲委派下的Class文件加载流程

总结一下,比如说现在要去加载个String.class文件,我们自己定义了一个CustomerClassLoader

  • 首先会判断自己的CustomerClassLoader否加载过,如果加载过直接返回,
  • 如果没有加载过则会调用父类PathClassLoader去加载,该父类同样会判断自己是否加载过,如果没有加载过则委托给父类BootClassLoader去加载,
  • 这个BootClassLoader是顶级classLoader,同样会去判断自己有没有加载过,如果也没有加载过则会调用自己的findClass(name)去加载,
  • 如果顶级BootClassLoader加载失败了,则会把加载这个动作向下交还给PathClassLoader,
  • 这个PathClassLoader也会尝试去调用findClass(name);去加载,如果加载失败了,则会继续向下交还给CustomClassLoader来完成加载
    这整个过程感觉是一个递归的过程,逐渐往上然后有逐渐往下,直到加载成功
    其实这个String.class在系统启动的时候已经被加载了,我们自己定义一个CustomerClassLoader去加载,其实也是父类加载的
双亲委派的作用
  • 1.防止同一个.class文件重复加载
  • 2.对于任意一个类确保在虚拟机中的唯一性.由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性
  • 3.保证.class文件不被篡改,通过委托方式可以保证系统类的加载逻辑不被篡改.

3.Android中的主要类加载器

  • 1.PathClassLoader复杂的加载系统类和英勇程序的类,通常用来加载已安装apkdex文件,实际上外部存储的dex文件也能加载
  • 2.DexClassLoader可以加载dex文件以及包含dex的压缩文件(apk,dex,jar,zip)
  • 3.BaseDexClassLoader实际应用层类文件的加载,而真正的加载逻辑委托给pathList来完成
  • 4.BootClassLoaderAndroid平台上所有ClassLoader的最终parent,Android系统启动时会使用BootClassLoader来预加载常用类

4.PathClassLoaderDexClassLoader 到底有何不同以及源码解析


我们可以发现PathClassLoaderDexClassLoader都继承自BaseDexClassLoader,然后BaseDexClassLoader继承自ClassLoader,
具体源码如下

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);
    }
}
public class DexClassLoader extends BaseDexClassLoader {
    //dexpath:dex文件以及包含dex的apk文件或jar文件的路径,多个路径用文件分隔符分割,默认文件分隔符为 ': '
    //optimizedDirectory:Android系统将dex文件进行优化后所生产的ODEX文件的存放路径,该路径必须是一个
                                    //PathClassLoader中默认使用路径"/data/dalvik-cache"
                                    //而DexClassLoader则需要我们指定ODEX文件的存放路径
    //librarySearchPath:所使用到的C/C++库存放的路径
    //parent:这个参数的主要作用是保留java中ClassLoader的委托机制
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
         super(dexPath, null, librarySearchPath, parent);
    }
}

阅读源码,我们可以发现PathClassLoaderDexClassLoader都没有重写findClass(name);方法,仅仅只是暴露出来1-2个构造方法而已,他俩啥事也没干,在Android 8.0以后两个类功能是一样的
参数解析

  • 1.dexpath:dex文件以及包含dexapk文件或jar文件的路径,多个路径用文件分隔符分割,默认文件分隔符为': '
  • 2.optimizedDirectory:Android系统将dex文件进行优化后所生产的ODEX文件的存放路径,该路径必须是一个内部存储路径
    PathClassLoader中默认使用路径"/data/dalvik-cache"
    而DexClassLoader则需要我们指定ODEX文件的存放路径
  • 3.librarySearchPath:所使用到的C/C++库存放的路径
  • 4.parent:这个参数的主要作用是保留java中ClassLoader的委托机制

这个PathClassLoader主要是用来加载APK中的文件的,然后有没有重新findClass(name);来实现文件加载,我们就去父类BaseDexClassLoader中找该方法,

public class BaseDexClassLoader extends ClassLoader {

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, 
                              ClassLoader parent,boolean isTrusted) {
        super(parent);
        ......
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        ......
    }

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

    //资源文件的加载
    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }

    //class文件的加载
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        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;
    }
}

我们阅读上边的源码可以发现,findClass(String name)findResource(String name)findLibrary(String name)等方法都是通过pathList这个对象来进行加载的,然后这个pathList对象是在构造函数中创建的,来具体看下

final class DexPathList {

   /** list of dex/resource (class path) elements */
    private final Element[] dexElements;

   /** list of native library directory elements */
    private final File[] nativeLibraryDirectories;

    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory,boolean isTrusted) {
        ......
        //1.根据传进来的dexPath路径加载出来所有的dex文件
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
        //2.根据libraryPath加载出来所有的动态库文件
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

    /**
     * Element of the dex/resource file path
     */
    /*package*/ static class Element {
        public final File file;
        public final ZipFile zipFile;
        public final DexFile dexFile;
        public Element(File file, ZipFile zipFile, DexFile dexFile) {
            this.file = file;
            this.zipFile = zipFile;
            this.dexFile = dexFile;
        }
        public URL findResource(String name) {
            if ((zipFile == null) || (zipFile.getEntry(name) == null)) {
                return null;
            }
            try {
                return new URL("jar:" + file.toURL() + "!/" + name);
            } catch (MalformedURLException ex) {
                throw new RuntimeException(ex);
            }
        }
    }
}

DexPathList创建的时候 主要干了

  • 1.根据传进来的dexPath路径加载出来所有的dex文件,重要参数dexElements
  • 2.根据libraryPath加载出来所有的动态库文件

来我们继续看dexElements是如何被加载进来的,具体看下makeDexElements(splitDexPath(dexPath), optimizedDirectory);这个方法
首先会先调用splitDexPath(dexPath)对传进来的dexPath做一个分割,然后会返回一个File的集合

final class DexPathList {
    private static ArrayList splitDexPath(String path) {
        return splitPaths(path, false);
    }
    private static List splitPaths(String searchPath, boolean wantDirectories) {
       List result = new ArrayList<>();
       if(searchPath!=null){
        for(String path : searchPath.split(File.pathSeparator)){
            if(directoriesOnly){
                try{
                      StructStat sb = Libcore.os.stat(path);
                      if(!S_ISDIR(sb.st_mode)){
                           continue;
                       }
                }catch(ErrnoException ignored){
                    continue;
                }
            }
            result.add(new File(path));
        }
      }
    }
}

接下来看makeDexElements方法


    private static Element[] makeDexElements(List files, File optimizedDirectory,
            List suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
      * 遍历传进来的File集合
       */
      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();
              DexFile dex = null;
              //判断是否是已 .dex结尾的
              if (name.endsWith(DEX_SUFFIX)) {
                     dex = loadDexFile(file, optimizedDirectory, loader, elements);
             } else {//不是以.dex结尾的,有可能是zip或者jar文件,照样跟上面的逻辑一样
                    dex = loadDexFile(file, optimizedDirectory, loader, elements);
              }
         } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        //android 8.0以后 optimizedDirectory 是废弃掉的
        if (optimizedDirectory == null) {
            return new DexFile(file,loader,elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0,loader,elements);
        }
    }

该方法内部主要是一个for循环把应用中的所有dex文件遍历了出来,被加载出来之后都会存储到dexElements这个数组中,这个dex文件可以理解为一个一个的文件夹,这个文件夹中含有很多class文件


app可以把启动的.class文件集中打进第一个dex包中,这是启动优化的一个思路
然后我们这个第一个dex文件夹和下面的classes2.dex中都包含一个MainActivity.class文件,则会优先加载第一个,这也就是热修复的鼻祖方案

5.1Class文件加载步骤和源码分析

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并且提供了访问方法区内的数据结构的方法.

  • 1.通过Class.forName()方法动态加载
  • 2.通过ClassLoader.loadClass()方法动态加载
    类的加载分为3个步骤:1.装载(Load),2.链接(Link),3.初始化(Intialize)

1.装载(Load)查找并加载类的二进制数据(查找和导入Class文件)

  • 通过一个类的全限定名来获取其定义的二进制字节流
  • 将这个字节流转化为方法区的运行时数据结构
  • Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口

2.链接(Link)
验证:确保被加载的类的正确性

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;比如说,是否以0xCAFEBABE开头,主次版本是不在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,比如说这个类是否有父类,除了java.lang.Object之外
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的

准备:为类的静态变量分配内存,并将其初始化为默认值

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时候随着对象一块分配在java堆
  • 这里所设置的初始值通常情况下是数据类型默认的零值,(如0,0L,null,false),而不是被在java代码中被显示地赋予的值

解析:把类中的符号引用转换为直接引用

  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类,接口,字段,类方法,接口方法,方法类型.符号引用就是一组符号来描述目标,可以是任何字面量.直接引用就是直接指向目标的内存地址指针

3.初始化(Intialize):指向类的方法,对类的静态变量,静态代码块执行初始化操作(不是必须的)
这个初始化不是执行构造函数,也不是执行字节码中的init方法,是执行的类构造器的cinit方法,

  • cinit方法是在这个class在初始化的时候才会执行的
  • init方法是在这个class所对应的java对象被实例化的时候才会执行

以下方式会触发类的初始化

  • 1.创建类的实例,也就是new一个对象
  • 2.访问某个类或接口的静态变量,或者对该静态变量赋值
  • 3.调用类的静态方法
  • 4.反射Class.forName("android.app.ActivityThread")
  • 5.初始化一个类的子类(会首先初始化子类的父类)
  • 6.JVM启动时标明的启动类,即文件名和类名相同的那个类

5.2Class文件字节码分析

先抛一个问题,为什么静态方法不能访问非静态变量?,我们来通过一个简单的class类看一下字节码就能明白了

public class Test {
    public static int num = 3;
    public int num2 = 3;
    static void test(){
        System.out.println("test");
    }
}
.class public Lcom/yadong/day01_lsn01/Test;
.super Ljava/lang/Object;
.source "Test.java"

# static fields  声明的是静态的
.field public static num:I

# instance fields  声明的是非静态的
.field public num2:I

# direct methods
.method static constructor ()V
    .registers 1
    .line 12
    const/4 v0, 0x3
    # 静态的是在这里调用的 num,为num进行赋值
    sput v0, Lcom/yadong/day01_lsn01/Test;->num:I
    return-void
.end method

.method public constructor ()V
    .registers 2
    .line 9
    invoke-direct {p0}, Ljava/lang/Object;->()V
    .line 15
    const/4 v0, 0x3
    # 非静态的是在这里调用的 num2,为num2进行赋值
    iput v0, p0, Lcom/yadong/day01_lsn01/Test;->num2:I
    return-void
.end method

.method static test()V
    .registers 2
    .line 19
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "test"
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    .line 20
    return-void
.end method

我们能看出来,静态字段num和非静态字段num1是在不同的地方初始化的

  • 静态字段num 是在类构造器clinit方法内初始化并赋值的
  • 非静态字段num1是在init方法内初始化并赋值的,只有java实例化对象实例的时候才会执行init方法

为什么静态方法不能访问非静态变量?

public class MainActivity  {

    //在准备阶段他的值默认为0,初始化阶段才会被赋值为3
    //因为把value赋值为3的public static语句在编译后的指令是在类构造器()方法之中被调用的
    static int value =3;
    
    int value2 = 3;//随着对象实例化的时候,才会被赋值
    
    static void  test(){
        value2=100;//静态方法为什么不能访问非静态变量
    }
}

总结: 当我们在调用MainActivity.test()这个静态方法的时候,这时候并没创建MainActivity的实例对象,既然没有创建实例对象,那字段value2会被赋值么?肯定不会,那肯定不会访问

如何编译字节码步骤如下


6.1Class.forNameClassLoader.loadClass加载有何不同

  • Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static静态代码块.注意这里的静态块指的是在类初始化时的一些数据,但是classLoader却没有.
  • Class.forName()方法实际上也是调用的CLassLoader来实现的

总结一下就是:
虽然ClassLoader.loadClass也能加载一个类,但是不会触发类的初始化(也就是说不会对类的静态变量,静态代码块进行初始化操作),
Class.forName这种方式,不但会加载一个类,还会触发类的初始化阶段,也能够为这个类的静态变量,静态代码块进行初始化操作

6.2源码分析

先分析下Class.forName这种方式

public final class Class {
    public static Class forName(String className)throws ClassNotFoundException {
        //1.如果是在MainActivity中调用的Class.forName,则这个caller就是MainActivity.class对象
        Class caller = Reflection.getCallerClass();
        // 2.ClassLoader.getClassLoader(caller) 得到的是当前这个类(MainActivity.class)的ClassLoader对象
        return forName(className, true, ClassLoader.getClassLoader(caller));
    }

    /**
    *initialize为true:类被加载的时候会触发类的初始化操作
    */
    @CallerSensitive
    public static Class forName(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException {
        if (loader == null) {
            loader = BootClassLoader.getInstance();
        }
        Class result;
        try {
            //调用这个native层的方法得到result
            result = classForName(name, initialize, loader);
        } catch (ClassNotFoundException e) {
            Throwable cause = e.getCause();
            if (cause instanceof LinkageError) {
                throw (LinkageError) cause;
            }
            throw e;
        }
        return result;
    }

    @FastNative
    static native Class classForName(String className, boolean shouldInitialize,
            ClassLoader classLoader) throws ClassNotFoundException;

    public ClassLoader getClassLoader() {
        if (isPrimitive()) {
            return null;
        }
        return (classLoader == null) ? BootClassLoader.getInstance() : classLoader;
    }
}
public abstract class ClassLoader {
    // Returns the class's class loader, or null if none.
    static ClassLoader getClassLoader(Class caller) {
        if (caller == null) {
            return null;
        }
        return caller.getClassLoader();
    }
}

先看第一个方法public static Class forName(String className)throws ClassNotFoundException

  • 1.如果是在MainActivity中调用的Class.forName,则这个caller就是MainActivity.class对象
  • 2.ClassLoader.getClassLoader(caller) 得到的是当前这个类(MainActivity.class)的ClassLoader对象
  • 3.然后就调用了forName(className, true, ClassLoader.getClassLoader(caller));

继续追进去发现,调用这个native层的方法得到result
在native层的java_lang_Class.cp文件中,这行代码就是类的初始化

来看下ClassLoader.loadClass这种方式是如何实现的

public class BaseDexClassLoader extends ClassLoader {

  private final DexPathList pathList;

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        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;
    }
}
final class DexPathList {
    public Class findClass(String name, List suppressed) {
        for (Element element : dexElements) {
            Class clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

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

     static class Element {
           public Class findClass(String name, ClassLoader definingContext,List suppressed) {
               return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;
          }
    }
}
public final class DexFile {
   public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }
  private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

   private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)
            throws ClassNotFoundException, NoClassDefFoundError;
}

最后会走到这里,发现并没有做任何初始化类的操作
至此,全部分析完毕

你可能感兴趣的:(Android_类加载机制之双亲委派)