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)}. * */
* Note: In the Android reference implementation, the * second parameter of {@link #loadClass(String, boolean)} is ignored * anyway. *
* * @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 ClassloadClass(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 是基于 双亲代理模型的,具体关于 classloader 的详细解析,可以查看我的这篇文章:Android 插件化学习之路(二)之 ClassLoader 完全解析
简单来说就是 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 文件的类
所以,QQ 空间正是基于 ClassLoader 的这个原理,把有问题的类打包到一个 dex(patch.dex)中去,然后把这个 dex 插入到 Elements 的最前面 <喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="这里写图片描述" src="/uploadfile/Collfiles/20170317/20170317100644202.png" title="\" />
关于如何进行 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 标志。
采用 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。然后从 classpath 中找到 LoadBugClass,拿到其构造方法,在其中插入一行代码。
到这里插入代码的操作已经完成,但是还存在另外一个问题,那就是如何在 dx 之前去进行上述脚本的操作?
答案就在 HotFix 的 app/build.gradle 中
- apply plugin: 'com.android.application'task('processWithJavassist') << {
- String classPath = file('build/intermediates/classes/debug') //项目编译class所在目录 dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir .absolutePath + '/intermediates/classes/debug')//第二个参数是hackdex的class所在目录}buildTypes { debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' }}applicationVariants.all { variant -> variant.dex.dependsOn << processWithJavassist //在执行dx命令之前将代码打入到class中}
可以看到在 build.gradle 中,在执行 dx 之前,会先执行 processWithJavassist 这个任务。这样会执行 PatchClass.groovy 的脚本,在构造方法中进行注入
将修复的. dex 文件插入 dexElements
寻找 class 是遍历 dexElements;然后我们的 AntilazyLoad.class 实际上并不包含在 apk 的 classes.dex 中,并且根据上面描述的需要,我们需要将 AntilazyLoad.class 这个类打成独立的 hack_dex.jar,注意不是普通的 jar,必须经过 dx 工具进行转化。
具体做法:
- jar cvf hack.jar dodola / hackdex
- /* dx --dex --output hack_dex.jar hack.jar */
还记得之前我们将所有的类的构造方法中都引用了 AntilazyLoad.class,所以我们需要把 hack_dex.jar 插入到 dexElements,而在 hotfix 中,就是在 Application 中完成这个操作的
- ublic class HotfixApplication extends Application {@Override public void onCreate() {
- super.onCreate();
- File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
- Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
- HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
- try {
- this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
- } catch(ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- }
在 app 的私有目录创建一个文件,然后调用 Utils.prepareDex 将 assets 中的 hackdex_dex.jar 写入该文件。 Utils.prepareDex 中其实就是文件的读写操作,注意:前提是你把 hackdex_dex.jar 放入到 assets 中
- public class Utils {
- private static final int BUF_SIZE = 2048;
- public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
- BufferedInputStream bis = null;
- OutputStream dexWriter = null;
- try {
- bis = new BufferedInputStream(context.getAssets().open(dex_file));
- dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
- byte[] buf = new byte[BUF_SIZE];
- int len;
- while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
- dexWriter.write(buf, 0, len);
- }
- dexWriter.close();
- bis.close();
- return true;
- } catch(IOException e) {
- if (dexWriter != null) {
- try {
- dexWriter.close();
- } catch(IOException ioe) {
- ioe.printStackTrace();
- }
- }
- if (bis != null) {
- try {
- bis.close();
- } catch(IOException ioe) {
- ioe.printStackTrace();
- }
- }
- return false;
- }
- }
- }
接下来 HotFix.patch 就是去反射去修改 dexElements 了
- public static void patch(Context context, String patchDexFile, String patchClassName) {
- if (patchDexFile != null && new File(patchDexFile).exists()) {
- try {
- if (hasLexClassLoader()) {
- injectInAliyunOs(context, patchDexFile, patchClassName);
- } else if (hasDexClassLoader()) {
- injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
- } else {
- injectBelowApiLevel14(context, patchDexFile, patchClassName);
- }
- } catch(Throwable th) {}
- }
- }
可以看到 patch 方法中有几个分支,说白了是根据不同的系统中 ClassLoader 的类型来做相应的处理
- private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException,
- NoSuchMethodException,
- IllegalAccessException,
- InvocationTargetException,
- InstantiationException,
- NoSuchFieldException {
- PathClassLoader obj = (PathClassLoader) context.getClassLoader();
- String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
- Class cls = Class.forName("dalvik.system.LexClassLoader");
- Object newInstance = cls.getConstructor(new Class[] {
- String.class,
- String.class,
- String.class,
- ClassLoader.class
- }).newInstance(new Object[] {
- context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
- context.getDir("dex", 0).getAbsolutePath(),
- patchDexFile,
- obj
- });
- cls.getMethod("loadClass", new Class[] {
- String.class
- }).invoke(newInstance, new Object[] {
- patchClassName
- });
- setField(obj, PathClassLoader.class, "mPaths", appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
- setField(obj, PathClassLoader.class, "mFiles", combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
- setField(obj, PathClassLoader.class, "mZips", combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
- setField(obj, PathClassLoader.class, "mLexs", combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
- }
上述方法中的 LexClassLoader 应该是阿里自己的 ClassLoader,可以看到上面将修复的文件的结尾都换成了. lex 的结尾,这些文件就是专门需要通过 LexClassLoader 进行加载的
我们分 API 14 以上和以下进行分析
API 14 以下
- private static void injectBelowApiLevel14(Context context, String str, String str2) throws ClassNotFoundException,
- NoSuchFieldException,
- IllegalAccessException {
- PathClassLoader obj = (PathClassLoader) context.getClassLoader();
- DexClassLoader dexClassLoader = new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
- dexClassLoader.loadClass(str2);
- setField(obj, PathClassLoader.class, "mPaths", appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath")));
- setField(obj, PathClassLoader.class, "mFiles", combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles")));
- setField(obj, PathClassLoader.class, "mZips", combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips")));
- setField(obj, PathClassLoader.class, "mDexs", combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs")));
- obj.loadClass(str2);
- }
通过 setField 方法将 mPaths 属性,修改为通过 appendArray 方法创造的新元素
- private static Object getField(Object obj, Class cls, String str) throws NoSuchFieldException,
- IllegalAccessException {
- Field declaredField = cls.getDeclaredField(str);
- declaredField.setAccessible(true);
- return declaredField.get(obj);
- }
- private static Object appendArray(Object obj, Object obj2) {
- Class componentType = obj.getClass().getComponentType();
- int length = Array.getLength(obj);
- Object newInstance = Array.newInstance(componentType, length + 1);
- Array.set(newInstance, 0, obj2);
- for (int i = 1; i < length + 1; i++) {
- Array.set(newInstance, i, Array.get(obj, i - 1));
- }
- return newInstance;
- }
而 appendArray 中就是创建一个新的 Array,把 obj2 插入到 obj 的前面,注意这里的 obj2 长度只有 1
所以,在 injectBelowApiLevel14 的以下方法中,就是把 mRawDexPath 的元素插入到 mPaths 中所有元素之前,而重新组合而成的新 mPaths 替换掉旧的 mPaths
- setField(obj, PathClassLoader.class, "mPaths", appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath") ));
接下来的替换,是通过 combineArray 生成的新元素替换掉旧元素,这里分别是 mFiles,mZips,mDexs
- setField(obj, PathClassLoader.class, "mFiles", combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles")));
- setField(obj, PathClassLoader.class, "mZips", combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips")));
- setField(obj, PathClassLoader.class, "mDexs", combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs")));
于是我们需要看一下 combineArray 方法里面做了什么
- private static Object combineArray(Object obj, Object obj2) {
- Class componentType = obj2.getClass().getComponentType();
- int length = Array.getLength(obj2);
- int length2 = Array.getLength(obj) + length;
- Object newInstance = Array.newInstance(componentType, length2);
- for (int i = 0; i < length2; i++) {
- if (i < length) {
- Array.set(newInstance, i, Array.get(obj2, i));
- } else {
- Array.set(newInstance, i, Array.get(obj, i - length));
- }
- }
- return newInstance;
- }
逻辑也很简单,也就是两个数组的合并而已
API14 以上
- private static void injectAboveEqualApiLevel14(Context context, String str, String str2) throws ClassNotFoundException,
- NoSuchFieldException,
- IllegalAccessException {
- PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
- Object a = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
- Object a2 = getPathList(pathClassLoader);
- setField(a2, a2.getClass(), "dexElements", a);
- pathClassLoader.loadClass(str2);
- }
根据 context 拿到 PathClassLoader,然后通过 getPathList(pathClassLoader),拿到 PathClassLoader 中的 pathList 对象,在调用 getDexElements 通过 pathList 取到 dexElements 对象。
- private static Object getDexElements(Object obj) throws NoSuchFieldException,
- IllegalAccessException {
- return getField(obj, obj.getClass(), "dexElements");
- }
- private static Object getPathList(Object obj) throws ClassNotFoundException,
- NoSuchFieldException,
- IllegalAccessException {
- return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
- }
同样是通过 combineArray 方法,对数组进行合并,合并完成后,将新的数组通过反射的方式设置给 pathList.
通过上面的一系列流程,那么 hack_dex.jar 已经插入到 dexElements 最前面了,补丁插入的过程也和 hack_dex.jar 的插入流程是一致的
到这里,dex 分包方案实现热修复的 HotFix 的分析就已经完毕了。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: http://www.92to.com/bangong/2017/03-17/18933686.html