目录
2、修改好代码之后,把这个java文件编译成.class文件
3、打包,把修改好的.class文件使用dx.bat工具打包成
4、加载dex包到用户端(通过网络去自己的服务器下载,测试的时候我们直接放入到手机里面,通过程序去读取)
5、(程序读取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角标0位置
1)形参patch是补丁的路径,先去获取ClassLoader。
3)通过同样的方法获取到DexPathList中的集合dexElements,这是dex真正的藏身之地
Qzone热修复技术是一个冷启动修复方式,为什么是冷启动呢,这需要根据它的实现原理来解析这个问题:
这种修复方式在于我们的代码编译成class文件之后,并打包成的dex文件的运行机制。我们都知道我们的虚拟机在执行我们的代码的时候,是从dex包中获取class文件进行读取的,而一个apk不止有一个dex文件,寻找class文件的时候,通过循环遍历所有dex文件,找到了某个class,无论后面有没有文件,循环都会中止,通过这个原理 我们可以使用插入补丁包的方式,把补丁dex包插入到dex集合最前面,这样虚拟机在寻找之前出bug的文件之前,首先会找到这个补丁包下面的 我们已经修改过的class,原先的有bug的dex不会被执行到,问题也就解决了
实际操作
理论是理论,实际的还需要实战去操作,我们先说一下具体的操作流程:
1、修改有BUG的代码
这一步,就是更改代码,把有问题的java类里面有问题的的代码更改过来,无论几个类
2、修改好代码之后,把这个java文件编译成.class文件
这一步也很简单,
1)、可以使用编译工具
build一下,然后在 项目➡️APP➡️build➡️intermediates➡️classes文件夹debug或者release中找到对应的已经编译好的.class文件(在编译工具的工具栏有一个 build 按钮,点击弹出列表找到build apk(s),点击)
2)、通过命令行工具,执行java命令进行编译
javac classpath/class1.java classpath/class2.java
(文件之间需要空一格,以便区分)
3、打包,把修改好的.class文件使用dx.bat工具打包成
dx.bat工具位于sdk目录下的build-tools下(Android\sdk\build-tools\27.0.3)
打包命令:
dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]
例如: --dex --output=/Users/**/Desktop/testDex.dex TextUtil.class TextUtil2.class
第一个参数是打包后的dex放置位置,后面的是需要打包的class文件 多个文件之间用空格隔开
注意 如果提示 dx命令找不到。说明没配置环境变量 要么去配置环境变量或者去de .bat的目录中去运行这个命令
我是先把.class文件打包成jar格式 然后在转换成dex
提醒大家一句:我的打包jar跟转换dex,都是在项目的debug或者release文件下操作的,(否则会引起java文件的路径问题)
1)打包jar
jar cvf patch.jar com/jjf/hotfixdemo2/TextUtil.class com/jjf/hotfixdemo2/TextUtil.class com/jjf/hotfixdemo2/TextUtil.class
2)jar转换成dex
dx --dex --output=patch.dex patch.jar
因为所有的操作都在debug或者release下操作的,而我们是直接指定的文件名 并没有写路径,所以产生的文件都在当前目录下,还有一个原因,每个java类都有一个
package com.**.**;
来说明自己在项目中的路径,如果不在这里操作,编译成.dex文件时会提示java文件路径错误(如果只是测试两个java类,不涉及Android,跟本文章瘦的热更新无关,可以删掉这行去手动编译成class,并打包如果其中有集成JFrame,并在main方法调用了窗口,则可以直接双击这个jar运行,会出现一个你编辑的窗口)
4、加载dex包到用户端(通过网络去自己的服务器下载,测试的时候我们直接放入到手机里面,通过程序去读取)
因为是做demo,这一步我直接把打包好的dex文件放入了提前准备好的手机内存中的某一路径,在代码中直接去读取这个路径,
public class MyApplication extends Application { @SuppressLint("SdCardPath") @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base);//hotfixTextAddress 测试热修复补丁地址 String address = Environment.getExternalStorageDirectory() + "/测试热修复补丁地址/patch.jar"; File file = new File(address); Log.i("TextUtil", "" + file.exists()); if (file.exists()) { //把布丁插入到pathList中(pathList进程变量,一个存放dex编译文件的集合) HiAppFix.installPatch(this, address); } } }
这个加载的步骤,最好放在启动页 初始化的时候去完成,在实际项目中的流程:
5、(程序读取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角标0位置
这一步是核心的代码。涉及到了代码具体加载的步骤,直接上代码,然后再一步一步解读,大家也可以去看看我画的热更新思维导图,里面有对源码的解释总结
private static final String TAG = "TextUtil"; /** * 执行修复 */ public static void installPatch(Context context, String patch) { //优化目录必须是私有目录 File cacheDir = context.getCacheDir(); //PathClassLoader ClassLoader classLoader = context.getClassLoader(); try { //先获取pathList属性 Field pathList = SharedReflectUtils.getField(classLoader, "pathList"); //通过属性反射获取属性的对象 DexPathList Object pathListObject = pathList.get(classLoader); //通过 pathListObject 对象获取 pathList类中的dexElements 属性 //原本的dex element数组 Field dexElementsField = SharedReflectUtils.getField(pathListObject, "dexElements"); //通过dexElementsField 属性获取它存在的对象 Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject); List<File> files = new ArrayList<>(); File file = new File(patch);//补丁包 if(file.exists()){ files.add(file); } //插桩所用到的类 // files.add(antiazyFile); Method method = SharedReflectUtils.getMethod(pathListObject, "makeDexElements", List.class, File.class, List.class,ClassLoader.class); final List<IOException> suppressedExceptionList = new ArrayList<IOException>(); //补丁的element数组 Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader); //用于替换系统原本的element数组 Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(), dexElementsObject.length + patchElement.length); //合并复制element System.arraycopy(patchElement, 0, newElement, 0, patchElement.length); System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length); // 替换 dexElementsField.set(pathListObject,newElement); } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); Log.i(TAG,"installPatch="+e.toString()); } }
/** *修复工具类(反射) */ public class SharedReflectUtils { /** * 反射获取某个属性 * * @param instance * @param name * @return */ public static Field getField(Object instance, String name) throws NoSuchFieldException { for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) { try { Field declaredField = cls.getDeclaredField(name); //如果反射获取的类 方法 属性不是public 需要设置权限 declaredField.setAccessible(true); return declaredField; } catch (NoSuchFieldException e) { e.printStackTrace(); } } throw new NoSuchFieldException("Field: " + name + " not found in " + instance.getClass()); } /** * 反射获取某个属性 * * @param instance * @param name * @return */ public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) throws NoSuchFieldException { for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) { try { Method methodMethod = cls.getDeclaredMethod(name,parameterTypes); //如果反射获取的类 方法 属性不是public 需要设置权限 methodMethod.setAccessible(true); return methodMethod; } catch (NoSuchMethodException e) { e.printStackTrace(); } } throw new NoSuchFieldException("Field: " + name + " not found in " + instance.getClass()); } }
1)形参patch是补丁的路径,先去获取ClassLoader。
因为类加载器是完成热更新的核心类,这里我们要强调一下,类加载器ClassLoader 有两个子类BootClassLoader属于加载系统源码,我们不必理会,BaseDexClassLoader才是我们要关注的,这个类里面什么都没有,我们去使用过的是他的字类 DexClassLoader、PathClassLoader,DexClassLoader在API26之后,代码已经与PathClassLoader完全相同,但是默认使用PathClassLoader这个加载器去加载类
2)获取到DexPathList
private final DexPathList pathList; /** * @hide */ public void addDexPath(String dexPath) { pathList.addDexPath(dexPath, null /*optimizedDirectory*/); }
上面是BaseDexClassLoader中关于pathList 的源码,可以看出它的作用
Field.get(Object obj) 返回指定对象obj上此 Field 表示的字段的值
通过属性方法Field的get获取这个属性在ClassLoader中的值pathListObject,这才是真正的获取到了 DexPathList 的对象pathList,然后我们要通过这个对象来操作
3)通过同样的方法获取到DexPathList中的集合dexElements,这是dex真正的藏身之地
/** * Constructs an instance. 116 * 117 * @param definingContext the context in which any as-yet unresolved 118 * classes should be defined 119 * @param dexPath list of dex/resource path elements, separated by 120 * {@code File.pathSeparator} 121 * @param librarySearchPath list of native library directory path elements, 122 * separated by {@code File.pathSeparator} 123 * @param optimizedDirectory directory where optimized {@code .dex} files 124 * should be found and written to, or {@code null} to use the default 125 * system directory for same 126 */ 127 public DexPathList(ClassLoader definingContext, String dexPath, 128 String librarySearchPath, File optimizedDirectory) { 129 130 if (definingContext == null) { 131 throw new NullPointerException("definingContext == null"); 132 } 133 134 if (dexPath == null) { 135 throw new NullPointerException("dexPath == null"); 136 } 137 138 if (optimizedDirectory != null) { 139 if (!optimizedDirectory.exists()) { 140 throw new IllegalArgumentException( 141 "optimizedDirectory doesn't exist: " 142 + optimizedDirectory); 143 } 144 145 if (!(optimizedDirectory.canRead() 146 && optimizedDirectory.canWrite())) { 147 throw new IllegalArgumentException( 148 "optimizedDirectory not readable/writable: " 149 + optimizedDirectory); 150 } 151 } 152 153 this.definingContext = definingContext; 154 155 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); 156 // save dexPath for BaseDexClassLoader 157 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, 158 suppressedExceptions, definingContext); 159 160 // Native libraries may exist in both the system and 161 // application library paths, and we use this search order: 162 // 163 // 1. This class loader's library path for application libraries (librarySearchPath): 164 // 1.1. Native library directories 165 // 1.2. Path to libraries in apk-files 166 // 2. The VM's library path from the system property for system libraries 167 // also known as java.library.path 168 // 169 // This order was reversed prior to Gingerbread; see http://b/2933456. 170 this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); 171 this.systemNativeLibraryDirectories = 172 splitPaths(System.getProperty("java.library.path"), true); 173 List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); 174 allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); 175 176 this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories); 177 178 if (suppressedExceptions.size() > 0) { 179 this.dexElementsSuppressedExceptions = 180 suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); 181 } else { 182 dexElementsSuppressedExceptions = null; 183 } 184 }
我们要把补丁加载到这个集合当中去,可以借鉴addDexPath方法里面的流程
从上面的源码码可以看出,可以通过makeDexElements得到Element[]数组,那我们就把补丁传入反射出这个方法,并运行他,得到这个数组
4)操作dexElements,添加补丁包
关键代码在于这几行
Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader); //用于替换系统原本的element数组 Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(), dexElementsObject.length + patchElement.length); //合并复制element System.arraycopy(patchElement, 0, newElement, 0, patchElement.length); System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length); // 替换 dexElementsField.set(pathListObject,newElement);
最后的 dexElementsField.set(pathListObject,newElement); 不能忘记 这行代码的意思是 设置某对象(第一个参数)下的某个属性(第二个参数)的值,这里我们相当于直接把原来的数组覆盖了
6、bug修复
好了到了这里,我们就可以重启app
总结:这只是简单的实现热修复的其中一个方式,而且和没有具体涉及到业务部分,所以看起来很简单,实际上在热修复中还有一个不可忽视的问题,就是如果想要去适配4.4及之前的版本,还需要使用到插桩技术,这个涉及到了4.4 之前的编译优化问题,在后面的版本取消掉了,并且插桩技术在美团的热修复中也使用到了,今天说到的Qzone热修复属于冷启动修复,美团热修复技术属于热启动修复。下节我们再说有关插桩的部分-------------如果具体修复部分的代码没看懂,可以直接复制第五节5、(程序读取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角标0位置 这个里面的方法在Application中调用,前提是修复包你已经准备并放入了内存中。