Android ClassLoader

1,187 阅读6分钟

1. ClassLoader的定义

  • 将Class字节码转换成内存中的Class对象,实现Class的加载
    • Class字节码本质就是字节数组

2. Android ClassLoader 结构

ClassLoader.PNG

  • Android之中的ClassLoader有PathClassLoader和DexClassLoader
    • 这两种ClassLoader都派生自BaseDexClassLoader
  • BaseDexClassLoader是ClassLoader的子类
    • DexPathList是优化后的Dex列表
    • BootClassLoader是BaseDexClassLoader的内部类
    • URLClassLoader在android系统中无用

2.1. BootClassLoader

  • 和java虚拟机中不同的是BootClassLoader是ClassLoader内部类
  • 由java代码实现而不是c++实现
  • 是Android平台上所有ClassLoader的最终parent
  • 这个内部类是包内可见,所以我们没法使用

2.2. URLClassLoader

  • 只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器

2.3. DexClassLoader

  • DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载
  • 在BaseDexClassLoader里对JAR,ZIP,APK,DEX后缀的文件最后都会生成一个对应的DEX文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理

2.4. PathClassLoader

  • PathClassLoader将optimizedDirectory置为Null,也就是没设置优化后的存放路径
  • optimizedDirectory为null时的默认路径就是/data/dalvik-cache 目录

2.4.1. PathClassLoader和DexClassLoader的区别|API26之前

  • DexClassLoader可指定optimizedDirectory

2.4.2.PathClassLoader和DexClassLoader无区别| API26之后

  • 其实在Android8.0|API26之后,系统是不允许指定optimizedDirectory目录的,也就导致根本上无区别,源码如下:
public PathClassLoader(String dexPath, ClassLoader parent) {
    super((String)null, (File)null, (String)null, (ClassLoader)null);
    throw new RuntimeException("Stub!");
}

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super((String)null, (File)null, (String)null, (ClassLoader)null);
    throw new RuntimeException("Stub!");
}

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
    this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}

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

    if (reporter != null) {
        reportClassLoaderChain();
    }
}
    

3. BaseDexClassLoader

3.1. 构造参数

	//构造方法
	public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
    }
   	/**
     * @hide
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        if (reporter != null) {
            reportClassLoaderChain();
        }
    }
    /**
     * Reports the current class loader chain to the registered {@code reporter}.
     * The chain is reported only if all its elements are {@code BaseDexClassLoader}.
     */
    private void reportClassLoaderChain() {
        ArrayList<BaseDexClassLoader> classLoadersChain = new ArrayList<>();
        ArrayList<String> classPaths = new ArrayList<>();
        classLoadersChain.add(this);
        classPaths.add(String.join(File.pathSeparator, pathList.getDexPaths()));
        boolean onlySawSupportedClassLoaders = true;
        ClassLoader bootClassLoader = ClassLoader.getSystemClassLoader().getParent();
        ClassLoader current = getParent();
        while (current != null && current != bootClassLoader) {
            if (current instanceof BaseDexClassLoader) {
                BaseDexClassLoader bdcCurrent = (BaseDexClassLoader) current;
                classLoadersChain.add(bdcCurrent);
              	classPaths.add(String.join(File.pathSeparator, bdcCurrent.pathList.getDexPaths()));
            } else {
               onlySawSupportedClassLoaders = false;
                break;
            }
            current = current.getParent();
        }
        if (onlySawSupportedClassLoaders) {
            reporter.report(classLoadersChain, classPaths);
        }
    }
  • String dexPath
    • 目标类所在的APK或者JAR文件的全路径,类加载器对其进行加载
    • 如果要同时加载多个路径,通过**System.getProperty(“path.separtor”)**获取分隔符进行分割
    • 将dexPath路径上的文件ODEX优化到内部位置optimizedDirectory之中,对其进行加载
  • File optimizedDirectory
    • 由于DEX文件包含在JAR或者APK文件之中,所以要对其进行解压以获取DEX文件,此参数指定了解压目录
    • 当系统首次启动的时候直接获取的是解压后的DEX(ODEX)
    • ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径。
  • String librarySearchPath
    • 目标类之中的C/C++库的存放路径
  • ClassLoader parent
    • 该类加载器的父类加载器,一般为当前执行类的装载器,例如在Android中以context.getClassLoader()作为父装载器。

3.2. loadClass

  • 用于控制加载Class的入口
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}
  • 首先判断这个类是否之前被加载过,如果有则直接返回
  • 如果没有则首先尝试让parent ClassLoader进行加载,加载不成功才在自己的findClass中进行加载。

3.2.1. parents delegate

  • 和java虚拟机中常见的parents delegate模型一致的,这种模型并不是一个强制性的约束模型
  • 比如你可以继承ClassLoader复写loadCalss方法来破坏这种模型,只不过parents delegate是一种被推荐的实现类加载器的方式
  • jdk1.2以后已经不提倡用户在覆盖loadClass方法,而应该把自己的类加载逻辑写到findClass中。
  • parents delegate机制为的是放置类重复加载,先有父亲进行循环分析。

3.2.2. 共享与隔离

  • 实现共享,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
  • 实现隔离,不同继承路线上的ClassLoader加载的类肯定不是同一个类,这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。

3.3. findClass

  • 用于通过自己内部的逻辑获取目标类的字节码
	@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;
    }

	public Class<?> findClass(String name, List<Throwable> 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;
    }
  • pathList内部包含的就是ODEX集合(优化后的dex),通过findClass获取到所有dexElements,最终调用findClass获取Class。

3.4. defineClass|非Android平台

  • 用于将字节码转换成Class对象,但是在Android平台是不被允许的
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
    throws ClassFormatError
{
    throw new UnsupportedOperationException("can't load this type of class file");
}

4.ART虚拟机

Android Runtime(缩写为ART),在Android 5.0及后续Android版本中作为正式的运行时库取代了以往的Dalvik虚拟机。ART能够把应用程序的字节码转换为机器码,是Android所使用的一种新的虚拟机。

它与Dalvik的主要不同在于:

  • Dalvik采用的是JIT技术,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,
  • ART采用Ahead-of-time(AOT)技术,应用在第一次安装的时候,字节码就会预先编译成机器码,这个过程叫做预编译。ART同时也改善了性能、垃圾回收(Garbage Collection)、应用程序除错以及性能分析

但是,运行时内存占用空间较少同样意味着编译二进制需要更高的存储

  • ART模式相比原来的Dalvik,会在安装APK的时候,使用Android系统自带的dex2oat工具把APK里面的.dex文件转化成OAT文件,OAT文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容。这使得我们无需重新编译原有的APK就可以让它正常地在ART里面运行,也就是我们不需要改变原来的APK编程接口
  • ART模式的系统里,同样存在DexClassLoader类,包名路径也没变,只不过它的具体实现与原来的有所不同,但是接口是一致的。实际上,ART运行时就是和Dalvik虚拟机一样,实现了一套完全兼容Java虚拟机的接口
  • class:java 编译后的⽂件,每个类对应⼀个 class ⽂件
  • dex:Dalvik EXecutable 把 class 打包在⼀起,⼀个 dex 可以包含多个 class ⽂件
  • odex:Optimized DEX 针对系统的优化,例如某个⽅法的调⽤指令,会把虚拟的调⽤转换为使⽤具体的 index,这样在执⾏的时候就不⽤再查找了
  • oat:Optimized Android fifile Type。使⽤ AOT 策略对 dex 预先编译(解释)成本地指令,这样再运⾏阶段就不需再经历⼀次解释过程,程序的运⾏可以更快AOT:Ahead-Of-Time compilation 预先编译

5.DEX文件

  • class:java 编译后的⽂件,每个类对应⼀个 class ⽂件
  • dex:Dalvik EXecutable 把 class 打包在⼀起,⼀个 dex 可以包含多个 class ⽂件
  • odex:Optimized DEX 针对系统的优化,例如某个⽅法的调⽤指令,会把虚拟的调⽤转换为使⽤具体的 index,这样在执⾏的时候就不⽤再查找了
  • oat:Optimized Android fifile Type。使⽤ AOT 策略对 dex 预先编译(解释)成本地指令,这样再运⾏阶段就不需再经历⼀次解释过程,程序的运⾏可以更快
    • AOT:Ahead-Of-Time compilation 预先编译