第四章 android热更新系列文章 之 ClassLoader

android recyclerView动态改变GridLayoutManager android动态化_动态更新






目录

1. 前言

2. java中的classLoader

3. android 中的classLoader种类

4. android中的classLoader的特点

   4.1 双亲代理模型的特点

5. classLoader 的源码分析

6. Adnroid 动态加载的难点





1. 前言

热更新中最关键的部分就是classLoader





2. java中的classLoader

java中的classLoader种类和作用





3. android 中的classLoader种类

android recyclerView动态改变GridLayoutManager android动态化_加载_02




  • BootClassLoader
  • PathClassLoader
  • DexClassLoader
  • BaseDexClassLoader

名称

作用

备注

BootClassLoader

类似于java中的BootStrap ClassLoader,加载androidFrameWork层的字节码文件

PathClassLoader

类似于java中的APP ClassLoader,加载已经安装到系统中的APP的字节码文件

DexClassLoader

类似于java中的Custom ClassLoader,用来加载指定目录下的字节码文件

BaseDexClassLoader

是BootClassLoader、PathClassLoader、DexClassLoader的父类




4. android中的classLoader的特点




4.1 双亲代理模型的特点

classLoader在加载一个字节码文件的时候,首先会判断该类有没有被当前classLoader加载过,如果有直接使用,如果没有找到自己的父类(classLoader)判断有没有加载过,如果没有,就去找找父类的父类有没有加载过该文件,知道所有classLoader均为加载过该文件才会进行加载。

  • 带来两个作用:
  • a 类加载的共享功能

一些framework层架的类一但被我们的顶层的classLoader加载过,就会被缓存在内存里面,以后任何地方使用都不需要重新加载

  • b 类加载的隔离功能

保证不同继承路线classLoadder加载的类不是同一个类,避免用户冒充系统写一个类,冒充系统的类。

怎样的类被认为是同一个类

必须是包名相同、类名相同、是被同一个classLoader加载的三个条件缺一不可

  • 双亲代理模型的特点

一段APP代码的的运行至少需要多少个classLoader

需要两个,分别是BootClassLoader和PathClassLoader,一个加载framework层字节码,一个加载来自APP中的字节码文件

测试代码:

ClassLoader classLoader = getClassLoader();
            if (classLoader != null) {
                LogUtil.e("currentClassLoader'Name:____" + classLoader.toString());
                while (classLoader.getParent() != null) {
                    LogUtil.e(classLoader.toString() + "parrentClassLoader'Name:____" + classLoader.getParent());
                    classLoader = classLoader.getParent();
                }
            }

文件输出:

dalvik.system.PathClassLoader[...]

parrentClassLoader'Name:____java.lang.BootClassLoader@23c1dc5





5. classLoader 的源码分析

【注意】本文所参考的源码基于Android 7.1.2_r36 及以前版本,在Android 8.0.0_r4 之后,BaseDexClassLoader 、DexClassLoader 源码有所变动

  • PathClassLoader和DexClassLoader的区别
  • 1、DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
  • 2、PathClassLoader只能加载系统中已经安装过的apk

PathClassLoader 源码
以下源码全部来自Android6.0.1

package dalvik.system;
	
	public class PathClassLoader extends BaseDexClassLoader {
	
	    /** 有兴趣的可以看看注释,故意没删
	    * Creates a {@code PathClassLoader} that operates on a given list of files
	    * and directories. This method is equivalent to calling
	    * {@link #PathClassLoader(String, String, ClassLoader)} with a
	    * {@code null} value for the second argument (see description there).
	    *
	    * @param dexPath the list of jar/apk files containing classes and
	    * resources, delimited by {@code File.pathSeparator}, which
	    * defaults to {@code ":"} on Android
	    * @param parent the parent class loader
	    */
	    public PathClassLoader(String dexPath, ClassLoader parent) {
	        super(dexPath, null, null, parent);
	    }
	    /**
	    * Creates a {@code PathClassLoader} that operates on two given
	    * lists of files and directories. The entries of the first list
	    * should be one of the following:
	JAR/ZIP/APK files, possibly containing a "classes.dex" file as
	    * well as arbitrary resources.
	    *
	Raw ".dex" files (not inside a zip file).
	    *
	    *
	    * The entries of the second list should be directories containing
	    * native library files.
	    *
	    * @param dexPath the list of jar/apk files containing classes and
	    * resources, delimited by {@code File.pathSeparator}, which
	    * defaults to {@code ":"} on Android
	    * @param libraryPath the list of directories containing native
	    * libraries, delimited by {@code File.pathSeparator}; may be
	    * {@code null}
	    * @param parent the parent class loader
	    */
	    public PathClassLoader(String dexPath, String libraryPath,
	            ClassLoader parent) {
	        super(dexPath, null, libraryPath, parent);
	    }
	}

DexClassLoader 源码

package dalvik.system;

import java.io.File;

/**
 * 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.
 */
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>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 libraryPath 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 libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

原因
DexClassLoader构造函数

//dexPath :dex路径
  //optimizedDirectory :指定输出dex优化后的odex文件,可以为null
  //libraryPath:动态库路径(将被添加到app动态库搜索路径列表中)
  //parent:制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。
  public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
      super(dexPath, new File(optimizedDirectory), libraryPath, parent);
  }

PathClassLoader构造函数

//dexPath :dex文件以及包含dex的apk文件或jar文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’
//optimizedDirectory :指定输出dex优化后的odex文件,可以为null
//libraryPath:动态库路径包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为null。(将被添加到app动态库搜索路径列表中)
//parent:制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。
public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath,
        ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}

DexClassLoader 与 PathClassLoader 构造函数区别就是多了个optimizedDirectory参数,指定odex文件的输出路径。
DexClassLoader可以加载dex文件以及包含dex的apk文件或jar文件,也支持从SD卡进行加载,这也就意味着DexClassLoader可以在应用未安装的情况下加载dex相关文件。因此,它是热修复和插件化技术的基础
来查看它的代码,如下所示。

libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

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

DexClassLoader构造方法的参数要比PathClassLoader多一个optimizedDirectory参数,参数optimizedDirectory代表什么呢?我们知道应用程序第一次被加载的时候,为了提高以后的启动速度和执行效率,Android系统会对dex相关文件做一定程度的优化,并生成一个ODEX文件,此后再运行这个应用程序的时候,只要加载优化过的ODEX文件就行了,省去了每次都要优化的时间,而参数optimizedDirectory就是代表存储ODEX文件的路径,这个路径必须是一个内部存储路径。
PathClassLoader没有参数optimizedDirectory,这是因为PathClassLoader已经默认了参数optimizedDirectory的路径为:/data/dalvik-cache。DexClassLoader 也继承自BaseDexClassLoader ,方法实现也都在BaseDexClassLoader中。

BaseDexClassLoader 构造方法

//类路径: /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

最终我们得到如下的结论,当我们执行加载一个class文件时整个的执行流程是这样的:




android recyclerView动态改变GridLayoutManager android动态化_Android热修复_03

  1. 加载一个class必须当前classLoader(PathClassLoader或者DexClassLoader)调用自己的loadClass(String className)方法
  2. loadClass方法是接口classLoader定义的,真正的实现是在 BaseDexClassLoader 中的,BaseDexClassLoaderloadClass() 方法做了如下事情,这里就是双亲委托模式最好的提现

2.1 首先调用自己的 findLoadedClass(String name) 方法,看自己有没有加载过这个class文件 ,没有执行2.2,如果加载过直接将加载过的字节码class文件返回,方法结束

2.2 查找自己父 classLoader 并调用其 findLoadedClass(String name) 方法,如果找到查看父类是否加载过这个class字节码文件,没有执行2.3,如果加载过直接将加载过的字节码class文件返回,方法结束

2.3 查看BootClassLoader 是否加载过这个class字节码文件,没有执行3,如果加载过直接将加载过的字节码class文件返回,方法结束

  1. 执行到这个过程,可以确认当前的class文件没有被加载过,当前classLoader调用 “BaseDexClassLoader” 的 findClass(String name) 方法查找对应class字节码文件,其中 findClass(String name) 方法的实现如下

3.1 BaseDexClassLoaderfindClass(name) 调用了DexPathList的 dexPathList.findClass(name,..) 方法,其中DexPathList 类的初始化是在BaseDexClassLoader的构造方法职工进行的,DexPathList是将de文件路径转化成文件的辅助类,可以将dex结尾的文件路径转换成文件,并生成Element数组

3.2 dexPathList 对象的创建是在BaseDexClassLoader 的构造方法中完成的,然后 dexPathList 调用了自己的 makeDexElements(splitDexPath(dexPath),optimizedDirectoryexcept,classLoader) 的将所有的dex路径转换成dexfile存到Element数组中,DexPathList类中 MakeDexElements()方法的内部实现如3.2.1

3.2.1 MakeDexElements() 方法的内部,将传入的文件路径分割遍历判断分割后的dex是一个文件还是文件夹;如果这个文件压缩文件进行解压缩,找到其中所有的dex文件存储到Elements 数组中,如果是文件夹,遍历获取其中所有的dex文件储到Elements 数组中,如果是dex文件,直接储到Elements 数组中,这一步调用完成,其实是获取到了一个载有dexFile文件的Elements数组

3.3 DexPathList的内部实现,dexPathList的findClass(Element[] elements)方法的内部,遍历Element数组拿到里面的每一个Element对象,获取对象中成员变量DexFile,并调用DexFile的 loadClassBinaryName(name,classLoader,exception)

3.4 dexFile类中 loadClassBinaryName(name,classLoader,exception) 方法调用了自己的 defineClass(name,classLoader,cookie,DexFile,exception)

3.5 DexFile类中的 defineClass(name,classLoader,cookie,DexFile,exception) 方法最终调用了DexFile类中的 defineClassNative(name,classLoader,cookie,DexFile,exception) 本地方法





6. Adnroid 动态加载的难点

许多组件需要注册才能使用(像activity、service等组件)

资源动态加载复杂

  • 资源在android中是用 id 索引的形式查找的,如果资源没有在清单文件中注册,报错资源找不到

总结:

android程序的运行需要一个上下文环境,这个正是第三方加载库需要解决的重点

文章参考:

Android解析ClassLoader(一)Java中的ClassLoader热修复——深入浅出原理与实现Android 插件化和热修复知识梳理