Android热修复之中有一步需将待修复的java文件转化为class文件再打包成dex文件,在测试过程中遇到一些问题,因此做个记录。
一、生成.class文件
1.先Build-->Clean Project 工程,展开build--->intermediates目录,此时有些目录是没有生成的
2.再build--Rebuild Project工程,此时会在intermediates目录下生成一个javac的文件夹
在javac文件夹下找到classes目录,找到自己需要修复的类,将其连带完整的包名路径复制到任意目录下,如将其复制到桌面的dex文件夹,删除其它不用修复的类
二、使用dx.bat工具生成dex
1.sdk安装目录下的buiild-tools--->任意sdk版本可找到dx.bat工具
2.使用命令生成dex文件,在dx.bat所在目录下打开命令窗口---->Shift+目录空白处点鼠标右键--->在此处打开命令窗口
3.输入命令:
dx --dex --output=D:\桌面\dex\classes3.dex D:\桌面\dex
命令解释:
--output=D:\桌面\dex\classes3.dex 指定输出路径
D:\桌面\dex 存放前面复制出来的class文件所在目录(注意是包括全路径的文件夹,也可以有多个class)
回车后成功在指定文件夹下生成dex文件
4.拿到待修复的dex文件就可以验证之后的热修复了
三、使用dx.bat工具时出现的错误
1.ERROR: No suitable Java found. In order to properly use the Android Developer
出现改错误的原因是我安装的jdk版本不对,我之前的安装的是jdk 12 但是在使用sdk工具时报上面的错不能使用,之后重现下载安装了jdk-8u211就可以了
四、热修复注意点:
1.应用每次杀进程后再次重启,已经修复的会失效,需要重新修复,因此需放在application中进行修复操作
2.修复的dex最好放在应用包名下的文件夹中,可通过文件输入输出流将sd卡中的文件搬迁到指定目录下
3.在AndroidManifest.xml声明读写权限后,还需动态申请读写权限
4.调试安装应用时,取消掉instant run这里的按钮,在setting--->build,Executio中,因为这个本身就有热修复的功能,所以为了不影响测试效果每次都重新安装应用,不勾选该按钮
五、
1.附上热修复工具类
package com.dex.main; import android.content.Context; import android.util.Log; import com.dn.fixutils.MyConstants; import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.HashSet; import dalvik.system.DexClassLoader; import dalvik.system.PathClassLoader; /** * @描述 热修复工具(只认后缀是dex、apk、jar、zip的补丁) */ public class DexFixUtils { private static final String TAG = "xuwei"; private static final String DEX_SUFFIX = ".dex"; private static final String APK_SUFFIX = ".apk"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; public static final String DEX_DIR = "odex"; private static final String OPTIMIZE_DEX_DIR = "optimize_dex"; private static HashSet<File> loadedDex = new HashSet<>(); static { loadedDex.clear(); } /** * 加载补丁,使用默认目录:data/data/包名/files/odex * * @param context */ public static void loadFixedDex(Context context) { loadFixedDex(context, null); } /** * 加载补丁 * * @param context 上下文 * @param patchFilesDir 补丁所在目录 Environment.getExternalStorageDirectory() */ public static void loadFixedDex(Context context, File patchFilesDir) { if (context == null) { return; } // 遍历所有的修复dex // File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置) File fileDir = patchFilesDir != null ? patchFilesDir : context.getDir(DEX_DIR,Context.MODE_PRIVATE);// data/data/包名/files/odex(这个可以任意位置) Log.d(TAG, "loadFixedDex: 查询的文件路径:"+fileDir.getAbsolutePath()); File[] listFiles = fileDir.listFiles(); for (File file : listFiles) { if (file.getName().startsWith("classes") && (file.getName().endsWith(DEX_SUFFIX) || file.getName().endsWith(APK_SUFFIX) || file.getName().endsWith(JAR_SUFFIX) || file.getName().endsWith(ZIP_SUFFIX))) { loadedDex.add(file);// 存入集合 Log.d(TAG, "loadFixedDex: 存入的文件:"+file.getName()); } } // dex合并之前的dex doDexInject(context, loadedDex); } private static void doDexInject(Context appContext, HashSet<File> loadedDex) { String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;// data/data/包名/files/optimize_dex(这个必须是自己程序下的目录) File fopt = new File(optimizeDir); if (!fopt.exists()) { fopt.mkdirs(); } try { // 1.加载应用程序的dex PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); for (File dex : loadedDex) { // 2.加载指定的修复的dex文件 DexClassLoader dexLoader = new DexClassLoader( dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录 fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁) null,// 加载dex时需要的库 pathLoader// 父类加载器 ); // 3.合并 Object dexPathList = getPathList(dexLoader); Object pathPathList = getPathList(pathLoader); Object leftDexElements = getDexElements(dexPathList); Object rightDexElements = getDexElements(pathPathList); // 合并完成 Object dexElements = combineArray(leftDexElements, rightDexElements); // 重写给PathList里面的Element[] dexElements;赋值 Object pathList = getPathList(pathLoader); setField(pathList, pathList.getClass(), "dexElements", dexElements); Log.d(TAG, "doDexInject: (合并)修复完成!"); } } catch (Exception e) { e.printStackTrace(); } } /** * 反射给对象中的属性重新赋值 */ private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException { Field declaredField = cl.getDeclaredField(field); declaredField.setAccessible(true); declaredField.set(obj, value); } /** * 反射得到对象中的属性值 */ private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } /** * 反射得到类加载器中的pathList对象 */ private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } /** * 反射得到pathList中的dexElements */ private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException { return getField(pathList, pathList.getClass(), "dexElements"); } /** * 数组合并 */ private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> componentType = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组) int j = Array.getLength(arrayRhs);// 得到原dex数组长度 int k = i + j;// 得到总数组长度(补丁数组+原dex数组) Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组 System.arraycopy(arrayLhs, 0, result, 0, i); System.arraycopy(arrayRhs, 0, result, i, j); return result; } /** * 两个数组合并,与上面的数组合并方法等同 * @param arrayLhs * @param arrayRhs * @return */ private static Object combineArray2(Object arrayLhs, Object arrayRhs) { Class<?> localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组) int j = i + Array.getLength(arrayRhs);// 得到原dex数组长度 Object result = Array.newInstance(localClass, j); for (int k = 0; k < j; ++k) { if (k < i) { Array.set(result, k, Array.get(arrayLhs, k)); } else { Array.set(result, k, Array.get(arrayRhs, k - i)); } } return result; } }
2.在application中的调用方式
package com.dex.main; import android.app.Application; import android.content.Context; import android.os.Environment; import android.support.multidex.MultiDex; public class MyApplication extends Application{ @Override public void onCreate() { super.onCreate(); } @Override protected void attachBaseContext(Context base) { // TODO Auto-generated method stub //修复默认路径下的dex文件(待修复的文件都需要移入该目录中) data/data/包名/files/odex DexFixUtils.loadFixedDex(base); //直接在拿sd卡中的dex文件进行修复,但是应用在每次重启后(杀进程)都需要重写修复一次,所以最好是将修复的dex文件放在包名下的文件目录中 // DexFixUtils.loadFixedDex(base,Environment.getExternalStorageDirectory()); super.attachBaseContext(base); } }