Android热修复学习之旅——HotFix完全解析

  • 格式:doc
  • 大小:264.50 KB
  • 文档页数:12

Android热修复学习之旅——HotFix完全解析Android dex分包原理介绍QQ空间热修复方案基于Android dex分包基础之上,简单概述android dex分包的原理就是:就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当classes.dex和classes1.dex中都具有同一个类的时候,那么classloader会选择加载哪个类呢?这要从classloader的源码入手,加载类是通过classloader的loadClass方法实现的,所以我们看一下loadClass的源码:/*** Loads the class with the specified name. Invoking this method is* equivalent to calling {@code loadClass(className, false)}.* <p>* <strong>Note:</strong> In the Android reference implementation, the* second parameter of {@link #loadClass(String, boolean)} is ignored* anyway.* </p>** @return the {@code Class} object.* @param className* the name of the class to look for.* @throws ClassNotFoundException* if the class can not be found.*/public Class<?> loadClass(String className) throws ClassNotFoundException { return loadClass(className, false);}protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {Class<?> clazz = findLoadedClass(className);if (clazz == null) {ClassNotFoundException suppressed = null;try {clazz = parent.loadClass(className, false);} catch (ClassNotFoundException e) {suppressed = e;}if (clazz == null) {try {clazz = findClass(className);} catch (ClassNotFoundException e) {e.addSuppressed(suppressed);throw e;}}}return clazz;}简单来说就是ClassLoader用loadClass方法调用了findClass方法,点进去发现findClass是抽象方法,而这个方法的实现是在它的子类BaseDexClassLoader中,而BaseDexClassLoader 重载了这个方法,得到BaseDexClassLoader,进入到BaseDexClassLoader类的findClass方法中#BaseDexClassLoader@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name);if (clazz == null) {throw new ClassNotFoundException(name);}return clazz;}#DexPathListpublic 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;}#DexFilepublic Class loadClassBinaryName(String name, ClassLoader loader) {return defineClass(name, loader, mCookie);}private native static Class defineClass(String name, ClassLoader loader, int cookie);一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类关于如何进行dex分包后面再单独开一篇博客进行分析。

CLASS_ISPREVERIFIED的问题采用dex分包方案会遇到的问题,也就是CLASS_ISPREVERIFIED的问题,简单来概括就是:在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED标志。

那么,我们要做的就是,阻止该类打上CLASS_ISPREVERIFIED的标志。

注意下,是阻止引用者的类,也就是说,假设你的app里面有个类叫做AClass,再其内部引用了BClass。

发布过程中发现BClass有编写错误,那么想要发布一个新的BClass类,那么你就要阻止AClass这个类打上CLASS_ISPREVERIFIED的标志。

也就是说,你在生成apk之前,就需要阻止相关类打上CLASS_ISPREVERIFIED的标志了。

如何阻止,简单来说,让AClass在构造方法中,去引用别的dex文件,比如:C.dex中的某个类即可。

所以总结下来,防止这个错误,只需要:1、动态改变BaseDexClassLoader对象间接引用的dexElements;2、在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。

热修复框架HotFix解析采用QQ空间的热修复方案而实现的开源热修复框架就是HotFix,说到了使用dex分包方案会遇到CLASS_ISPREVERIFIED问题,而解决方案就是在dx工具执行之前,将所有的class 文件,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。

注意:AntilazyLoad.class这个类是独立在hack.dex中。

dex分包方案实现需要关注以下问题:1.如何解决CLASS_ISPREVERIFIED问题2.如何将修复的.dex文件插入到dexElements的最前面那么如何达到这个目的呢?在HotFix中采用的javassist来达到这个目的,以下是HotFix中的PatchClass.groovy代码public class PatchClass {/*** 植入代码* @param buildDir 是项目的build class目录,就是我们需要注入的class所在地* @param lib 这个是hackdex的目录,就是AntilazyLoad类的class文件所在地*/public static void process(String buildDir, String lib) {println(lib)ClassPool classes = ClassPool.getDefault()classes.appendClassPath(buildDir)classes.appendClassPath(lib)//下面的操作比较容易理解,在将需要关联的类的构造方法中插入引用代码CtClass c = classes.getCtClass("dodola.hotfix.BugClass")if (c.isFrozen()) {c.defrost()}println("====添加构造方法====")def constructor = c.getConstructors()[0];constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")c.writeFile(buildDir)CtClass c1 = classes.getCtClass("dodola.hotfix.LoadBugClass")if (c1.isFrozen()) {c1.defrost()}println("====添加构造方法====")def constructor1 = c1.getConstructors()[0];constructor1.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")c1.writeFile(buildDir)}static void growl(String title, String message) {def proc = ["osascript", "-e", "display notification \"${message}\" with title \"${title}\""].execute()if (proc.waitFor() != 0) {println "[WARNING] ${proc.err.text.trim()}"}}}其实内部做的逻辑就是:通过ClassPool对象,然后添加classpath。