What is HotFix?

以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装(来自:安卓App热补丁动态修复技术介绍)

HotFix原理

首先HotFix原理是基于Android Dex分包方案的,而Dex分包方案的关键就是Android的ClassLoader体系。ClassLoader的继承关系如下:

HotFix介绍及HotFix开源项目推荐-编程之家
ClassLoader继承关系

这里我们可以用的是PathClassLoaderDexClassLoader,接下来看看这两个类的注释:

  • PatchClassLoader
    /**
    * Provides a simple {@link ClassLoader} implementation that operates on a list
    * of files and directories in the local file system, but does not attempt to
    * load classes from the network. Android uses this class for its system class
    * loader and for its application class loader(s).
    */

    这个类被用作系统类加载器和应用类(已安装的应用)加载器。

  • DexClassLoader
    /**
    * 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.
    */

    注释可以看出,这个类是可以用来从.jar文件和.apk文件中加载classed.dex,可以用来执行没有安装的程序代码。

通过上面的两个注释可以清楚这两个类的作用了,很显然我们要用的是DexClassLoader,对插件化了解的小伙伴们对这个类肯定不会陌生的,对插件化不了解的也没关系。下面会更详细的介绍。

我们知道了PathClassLoader和DexClassLoader的应用场景,接下来看一下是如何加载类的,看上面的继承关系这里两个类都是继承自BaseDexClassLoader,所以查找类的方法也在BaseDexClassLoader中,下面是部分源码:

/*** Base class for common functionality between various dex-based* {@link ClassLoader} implementations.*/
public class BaseDexClassLoader extends ClassLoader {/** structured lists of path elements */private final DexPathList pathList;//...some code@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class clazz = pathList.findClass(name);if (clazz == null) {throw new ClassNotFoundException(name);}return clazz;}//...some code
}

可以看到在findClass()方法中用到了pathList.findClass(name),而pathList的类型是DexPathList,下面看一下DexPathList的findClass()方法源码:

/*** A pair of lists of entries, associated with a {@code ClassLoader}.* One of the lists is a dex/resource path — typically referred* to as a "class path" — list, and the other names directories* containing native code libraries. Class path entries may be any of:* a {@code .jar} or {@code .zip} file containing an optional* top-level {@code classes.dex} file as well as arbitrary resources,* or a plain {@code .dex} file (with no possibility of associated* resources).** <p>This class also contains methods to use these lists to look up* classes and resources.</p>*/
/*package*/ final class DexPathList {/** list of dex/resource (class path) elements */private final Element[] dexElements;/*** Finds the named class in one of the dex files pointed at by* this instance. This will find the one in the earliest listed* path element. If the class is found but has not yet been* defined, then this method will define it in the defining* context that this instance was constructed with.** @return the named class or {@code null} if the class is not* found in any of the dex files*/public Class findClass(String name) {for (Element element : dexElements) {DexFile dex = element.dexFile;if (dex != null) {Class clazz = dex.loadClassBinaryName(name, definingContext);if (clazz != null) {return clazz;}}}return null;}
}

这个方法里面有调用了dex.loadClassBinaryName(name, definingContext),然后我们来看一下DexFile的这个方法:

/*** Manipulates DEX files. The class is similar in principle to* {@link java.util.zip.ZipFile}. It is used primarily by class loaders.* <p>* Note we don't directly open and read the DEX file here. They're memory-mapped* read-only by the VM.*/
public final class DexFile {/*** See {@link #loadClass(String, ClassLoader)}.** This takes a "binary" class name to better match ClassLoader semantics.** @hide*/public Class loadClassBinaryName(String name, ClassLoader loader){return defineClass(name, loader, mCookie);}private native static Class defineClass(String name, ClassLoader loader, int cookie);
}

好了,关联的代码全部贴上了,理解起来并不难,总结一下流程:BaseDexClassLoader中有一个DexPathList对象pathList,pathList中有个Element数组dexElements(Element是DexPathList的静态内部类,在Element中会保存DexFile的对象),然后遍历Element数组,通过DexFile对象去查找类。
更通俗的说:

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。(出自安卓App热补丁动态修复技术介绍)

so,通过上面介绍,我们可以将patch.jar(补丁包),放在dexElements数组的第一个元素,这样优先找到我们patch.jar中的新类去替换之前存在bug的类。

方案有了,但是我们还差一个步骤,就是防止类被打上CLASS_ISPREVERIFIED的标记

解释一下:在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记。A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。(引用自Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!)

O..O..OK,现在很清楚了,实现热修复,我们需要完成两个任务:

  1. 改变BaseDexClassLoader中的dexElements数组,将我们的patch.jar插入到dexElements数组的第一个位置。
  2. 在打包的时候,我们要阻止类被打上CLASS_ISPREVERIFIED标记

HotFix框架汇总

  • RocooFix(推荐)
  • Nuwa
  • HotFix
  • AndFix
  • Tinker_imitator(微信热更新方案实践)

RoccoFix使用问题

RocooFix的正确教程,不生成patch.jar请按这个步骤来执行
待更新

参考

安卓App热补丁动态修复技术介绍
Android dex分包方案
Android 热补丁动态修复框架小结
Android热补丁动态修复技术(一):从Dex分包原理到热补丁
Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
Android热补丁动态修复技术(四):自动化生成补丁——解决混淆问题

文/zyyoona7(简书作者)
原文链接:http://www.jianshu.com/p/6f0ae1e364d9
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。