一篇文章搞懂热修复类加载方案原理

6,269 阅读10分钟

ClassLoader 类型

Java 中的 ClassLoader 可以加载 jar 文件和 Class文件(本质是加载 Class 文件),这一点在 Android 中并不适用,因为无论 DVM 还是 ART 它们加载的不再是 Class 文件,而是 dex 文件。

Android 中的 ClassLoader 类型和 Java 中的 ClassLoader 类型类似,也分为两种类型,分别是系统 ClassLoader自定义 ClassLoader。其中 Android 系统 ClassLoader 包括三种分别是 BootClassLoaderPathClassLoaderDexClassLoader,而 Java 系统类加载器也包括3种,分别是 Bootstrap ClassLoaderExtensions ClassLoaderApp ClassLoader

BootClassLoader

Android 系统启动时会使用 BootClassLoader 来预加载常用类,与 Java 中的 BootClassLoader 不同,它并不是由 C/C++ 代码实现,而是由 Java 实现的,BootClassLoade 的代码如下所示

// libcore/ojluni/src/main/java/java/lang/ClassLoader.java
class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

    ...
}

BootClassLoaderClassLoader 的内部类,并继承自 ClassLoaderBootClassLoader 是一个单例类,需要注意的是 BootClassLoader 的访问修饰符是默认的,只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的

PathClassLoader

Android 系统使用 PathClassLoader 来加载系统类和应用程序的类,如果是加载非系统应用程序类,则会加载 data/app/$packagename下的 dex 文件以及包含 dex 的 apk 文件或 jar 文件,不管是加载哪种文件,最终都是要加载 dex 文件,在这里为了方便理解,我们将 dex 文件以及包含 dex 的 apk 文件或 jar 文件统称为 dex 相关文件。PathClassLoader 不建议开发直接使用。

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

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);
    }
}

PathClassLoader继承自 BaseDexClassLoader,很明显 PathClassLoader 的方法实现都在 BaseDexClassLoader 中。

PathClassLoader 的构造方法有三个参数:

  • dexPath:dex 文件以及包含 dex 的 apk 文件或 jar 文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’。
  • librarySearchPath:包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为 null
  • parent:ClassLoader 的 parent

DexClassLoader

DexClassLoader 可以加载 dex 文件以及包含 dex 的 apk 文件或 jar 文件,也支持从 SD 卡进行加载,这也就意味着 DexClassLoader 可以在应用未安装的情况下加载 dex 相关文件。因此,它是热修复和插件化技术的基础。

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

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

关于以上 ClassLoader 在 Android 系统中的创建过程,这里牵扯到 Zygote 进程,非本文的重点,故不在此进行讨论。

ClassLoader 继承关系

  • ClassLoader 是一个抽象类,其中定义了 ClassLoader 的主要功能。BootClassLoader 是它的内部类。
  • SecureClassLoader类和 JDK8 中的 SecureClassLoader 类的代码是一样的,它继承了抽象类 ClassLoaderSecureClassLoader 并不是 ClassLoader 的实现类,而是拓展了 ClassLoader 类加入了权限方面的功能,加强了 ClassLoader 的安全性。
  • URLClassLoader 类和 JDK8 中的 URLClassLoader 类的代码是一样的,它继承自 SecureClassLoader,用来通过URl路径从 jar 文件和文件夹中加载类和资源。
  • BaseDexClassLoader 继承自 ClassLoader,是抽象类 ClassLoader 的具体实现类,PathClassLoaderDexClassLoader 都继承它。

下面看看运行一个 Android 程序需要用到几种类型的类加载器

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var classLoader = this.classLoader

        // 打印 ClassLoader 继承关系
        while (classLoader != null) {
            Log.d("MainActivity", classLoader.toString())
            classLoader = classLoader.parent
        }
    }
}

MainActivity 的类加载器打印出来,并且打印当前类加载器的父加载器,直到没有父加载器,则终止循环。打印结果如下:

com.zhgqthomas.github.hotfixdemo D/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk"],nativeLibraryDirectories=[/data/app/com.zhgqthomas.github.hotfixdemo-2/lib/arm64, /oem/lib64, /system/lib64, /vendor/lib64]]]

com.zhgqthomas.github.hotfixdemo D/MainActivity: java.lang.BootClassLoader@4d7e926

可以看到有两种类加载器,一种是 PathClassLoader,另一种则是 BootClassLoaderDexPathList 中包含了很多路径,其中 /data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk 就是示例应用安装在手机上的位置。

双亲委托模式

类加载器查找 Class 所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该 Class 是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的BootstrapClassLoader,如果 BootstrapClassLoader 找到了该 Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。 这是 JDK 中 ClassLoader 的实现逻辑,Android 中的 ClassLoaderfindBootstrapClassOrNull 方法的逻辑处理上存在差异。

// ClassLoader.java

    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) {
                long t0 = System.nanoTime();
                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.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

上面的代码很容易理解,首先会查找加载类是否已经被加载了,如果是直接返回,否则委托给父加载器进行查找,直到没有父加载器则会调用 findBootstrapClassOrNull 方法。

下面看一下 findBootstrapClassOrNullJDKAndroid 中分别是如何实现的

// JDK ClassLoader.java

    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

JDKfindBootstrapClassOrNull 会最终交由 BootstrapClassLoader 去查找 Class 文件,上面提到过 BootstrapClassLoader 是由 C++ 实现的,所以 findBootstrapClass 是一个 native 的方法

// JDK ClassLoader.java

    private native Class<?> findBootstrapClass(String name);

在 Android 中 findBootstrapClassOrNull 的实现跟 JDK 是有差别的

// Android 
    private Class<?> findBootstrapClassOrNull(String name)
    {
        return null;
    }

Android 中因为不需要使用到 BootstrapClassLoader 所以该方法直接返回来 null

正是利用类加载器查找 Class 采用的双亲委托模式,所以可以利用反射修改类加载器加载 dex 相关文件的顺序,从而达到热修复的目的

类加载过程

通过上面分析可知

  • PathClassLoader 可以加载 Android 系统中的 dex 文件
  • DexClassLoader 可以加载任意目录的 dex/zip/apk/jar 文件,但是要指定optimizedDirectory

通过代码可知这两个类只是继承了 BaseDexClassLoader,具体的实现依旧是由 BaseDexClassLoader 来完成。

BaseDexClassLoader

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

public class BaseDexClassLoader extends ClassLoader {

    ...
    
    private final DexPathList pathList;

    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();
        }
    }
    
    ...
    
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
    
    ...
}

通过 BaseDexClassLoader 构造方法可以知道,最重要的是去初始化 pathList 也就是 DexPathList 这个类,该类主要是用于管理 dex 相关文件

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions); // 查找逻辑交给 DexPathList
        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 中最重要的是这个 findClass 方法,这个方法用来加载 dex 文件中对应的 class 文件。而最终是交由 DexPathList 类来处理实现 findClass

DexPathList

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

final class DexPathList {
    ...

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;
    
    ...
    
    
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
       ...

        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ...
    }

}

查看 DexPathList 核心构造函数的代码可知,DexPathList 类通过 Element 来存储 dex 路径 ,并且通过 makeDexElements 函数来加载 dex 相关文件,并返回 Element 集合

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

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) { // 判断是否是 dex 文件
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else { // 如果是 apk, jar, zip 等文件
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                    // 将 dex 文件或压缩文件包装成 Element 对象,并添加到 Element 集合中
                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

总体来说,DexPathList 的构造函数是将 dex 相关文件(可能是 dex、apk、jar、zip , 这些类型在一开始时就定义好了)封装成一个 Element 对象,最后添加到 Element 集合中

其实,Android 的类加载器不管是 PathClassLoader,还是 DexClassLoader,它们最后只认 dex 文件,而 loadDexFile是加载 dex 文件的核心方法,可以从 jar、apk、zip 中提取出 dex

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

    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;
    }

DexPathList 的构造函数中已经初始化了 dexElements,所以这个方法就很好理解了,只是对 Element 数组进行遍历,一旦找到类名与 name 相同的类时,就直接返回这个 class,找不到则返回 null

热修复实现

通过上面的分析可以知道运行一个 Android 程序是使用到 PathClassLoader,即 BaseDexClassLoader,而 apk 中的 dex 相关文件都会存储在 BaseDexClassLoaderpathList 对象的 dexElements 属性中。

那么热修复的原理就是将改好 bug 的 dex 相关文件放进 dexElements 集合的头部,这样遍历时会首先遍历修复好的 dex 并找到修复好的类,因为类加载器的双亲委托模式,旧 dex 中的存有 bug 的 class 是没有机会上场的。这样就能实现在没有发布新版本的情况下,修复现有的 bug class

手动实现热修复功能

根据上面热修复的原理,对应的思路可归纳如下

  1. 创建 BaseDexClassLoader 的子类 DexClassLoader 加载器
  2. 加载修复好的 class.dex (服务器下载的修复包)
  3. 将自有的和系统的 dexElements 进行合并,并设置自由的 dexElements 优先级
  4. 通过反射技术,赋值给系统的 pathList

热修复 Demo 推荐

可以参考 Github 上的这个项目

参考