首先,我们需要持有以下几个问题:
- 什么是热修复?它可以帮我解决什么问题?
- 热修复的产生背景?
- 热修复的基本原理是什么?
- 如何选择热修复框架?
- 热修复的注意事项
热修复与多渠道?自动化构建与热修复?
上面一共有7个问题,如果是新同学的话,后面两条可能不会很了解,建议自行补课学习。于是最基本的5个问题,我们必须明白,这是我们每个开发者学习一个新知识的基本需要做到的。
测试环境 :Android 9.0 -P
什么是热修复?它可以帮我解决什么问题?
其实简单来说,热修复就是一种动态加载技术,比如你线上某个产品此时出现了bug:
传统流程:debug->测试->发布新版 ->用户安装(各平台审核时间不一,而且用户需要手动下载或者更新)
集成热修复情况下:dubug->测试->推送补丁->自动下载补丁修复 (用户不知情况,自动下载补丁并修复)
对比下来,我们不难发现,传统流程存在这几大弊端:
- 发版代价大
- 用户下载安装的成本过高
- bug修复不及时,取决于各平台的审核时间等等
热修复产生背景?
- app发版成本高
- 用H5集成某些经常变动的业务逻辑,但这种方案需要学习成本,而且对于无法转为H5形式的代码仍旧是无法修复;
- Instant Run
上面三个原因中,我们主要来谈一下 Instant Run:
Android Studio2.0时,新增了一个 Instant Run的功能,而各大厂的热修复方案,在代码,资源等方面的实现都是很大程度上参考了Instant Run的代码。所以可以说 Instant Run 是推进Android 热修复的主因。
那 Instant Run内部是如何做到这一点呢?
1.构建一个新的 AssetManager(资源管理框架),并通过反射调用这个 addAssetPath,把这个完整的新资源加入到 AssetManager中,这样就得到了一个含有所有新资源的 AssetManager.
2.找到所有之前引用到原有AssetManager的地方,通过反射,把引用出替换为新的AssetManager.
热修复的原理是什么?
我们都知道热修复都相当于动态加载,那么动态加载到底动态在哪里了呢。
说到这个就躲不过一个关键点 ClassLoader(类加载器) ,所以我们先从Java开始。
我们都知道Java的类加载器有四种,分别为:
- Bootstarp ClassLoader
- Extension ClassLoader
- App ClassLoader 加载应用ClassLoader
- Custom ClassLoader 加载自己的class文件
类加载过程如下:
过程:加载-连接(验证-准备-解析)-初始化
1.加载
将类的信息(字节码)从文件中获取并载入到JVM的内存中
2.连接
验证:检查读入的结构是否符合JVM规范
准备:分配一个结构来存储类的信息
解析:将类的常量池中的所有引用改变成直接引用
3.初始化
执行静态初始化程序,把静态变量初始化成指定的值
其中用到的三个主要机制:
- 双亲委托机制
- 全盘负责机制
- 缓存机制
其实后面的两个机制都是主要从双亲委托机制延续而来。
在说明了Java 的ClassLoader之后,我们接下来开始Android的ClassLoader,不同于Java的是,Java中的ClassLoader可以加载 jar 文件和 Class文件,而Android中加载的是Dex文件,这就需要重新设计相关的ClassLoader类。所以Android 的ClassLoader 我们会说的详细一点
源码解析
在这里,顺便提一下,这里贴的代码版本是Android 9.0,在8.0以后,PathClassLoader和DexClassLoader并没有什么区别,因为唯一的一个区别参数 optimizedDirectory已经被废弃。
首先是 loadClass,也就是我们类加载的核心方法方法:
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 } //找到根加载器依然为null,只能自己加载了 if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } return c; }
这里有个问题,JVM双亲委托机制可以被打破吗?先保留疑问。
我们主要去看他的 findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
这个方法是一个null实现,也就是需要我们开发者自己去做。
从上面基础我们知道,在Android中,是有 PathClassLoader和 DexClassLoader,而它们又都继承与 BaseDexClassLoader,而这个BaseDexClassLoader又继承与 ClassLoader,并将findClass方法交给子类自己实现,所以我们从它的两个子类 PathClassLoader和 DexClassLoader入手,看看它们是怎么处理的。
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: * * <ul> * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. * <li>Raw ".dex" files (not inside a zip file). * </ul> * * 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 librarySearchPath 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 librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); } }
由注释看可以发现PathClassLoader被用来加载本地文件系统上的文件或目录,因为它调用的 BaseDexClassLoader的第二个参数为null,即未传入优化后的Dex文件。
注意:Android 8.0之后,BaseClassLoader第二个参数为(optimizedDirectory)为null,所以DexClassLoader与PathClassLoader并无区别
DexClassLoader
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 this parameter is deprecated and has no effect since API level 26. * @param librarySearchPath 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 librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); } }
DexClassLoader用来加载jar、apk,其实还包括zip文件或者直接加载dex文件,它可以被用来执行未安装的代码或者未被应用加载过的代码,也就是我们修复过的代码。
注意:Android 8.0之后,BaseClassLoader第二个参数为(optimizedDirectory)为null,所以DexClassLoader与PathClassLoader并无区别
从上面我们可以看到,它们都继承于BaseDexClassLoader,并且它们真正的实现行为都是调用的父类方法,所以我们来看一下BaseDexClassLoader.
BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader { private static volatile Reporter reporter = null; //核心关注点 private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { //classloader,dex路径,目录列表,内部文件夹 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(); } } ... 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); } //核心方法 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //异常处理 List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); //这里也只是一个中转,关注点在 DexPathList 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; } ... }
从上面我们可以发现,BaseDexClassLoader其实也不是主要处理的类,所以我们继续去查找 DexPathList.