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

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350