Android热修复(Hot Fix)案例全剖析(二)
2016-10-03 21:32
393 查看
在上篇博客中,我们初步了解了Android热修复的基本流程,具体可以看我的博客Android热修复(Hot Fix)案例全剖析(一),那么本篇博客,我将为大家全面剖析Android热修复的实现案例。
在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
ClassLoader去加载Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由于apk是一个类似于压缩包的东西,Android其实是使用一个优化的临时缓存目录optimizeDir(dex),专门把Dex文件解压进去,这样以后就从这个临时缓存目录中加载,提高效率。
在1代码中我们提到了loadFixedDex()方法,便是我们的核心热修复工具类,我给大家具体讲一下:
ClassLoader有一个简单的实现类-PathClassLoader。该类作为Android的默认的类加载器,本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
每个ClassLoader有一个pathList变量,是标识dex文件的路径,我们通过该路径加载dex文件,默认不分包的时候只有一个dex文件,当然谷歌在顶层设计时允许我们有多个dex文件。
ClassLoader去找optimizeDir(dex)目录,然后把目录添加到pathList里面去,接着去找目录下面的所有的dex文件,把这些dex文件当做一个数组放到dexElements中去,这样就可以有多个dex文件。
ClassLoader每加载一个类,它会先找classes.dex,如果找不到就去classes2.dex中找,如果里面又一个dex有问题,比如说classes2.dex出问题了,我们就需要弄一个修复的新的classes2.dex文件放到数组中去,替换掉有问题的;但是classes2.dex中可能有多个类,除了有问题的类,也可能有很多正确的类,我们在替换时没必要把所有的类都替换掉,所以我们只要替换有问题的类。
为此,我们可以采用一个策略,把新的替换的dex文件放到数组的最前面,最终数组的形态为:
这里解释下,ClassLoader类加载器先加载我们修复的正确的dex文件,然后顺序加载数组中其他的dex元素,到了最后加载到旧的classes2.dex元素,由于前面已经加载了更新的classes2.dex(更新的dex文件中只包含修复的class),那么旧的classes2.dex元素中的有Bug的class就不会再加载,而是只加载其余的没有错误的class。
整个流程其实非常简单,但是如果我们要实现这个过程却有个障碍,那就是由于我们的APK程序可能正在运行,谷歌并没有提供相关的接口方法去实现这一步骤,为此,我们需要使用反射的手段去实现。
1.首先需要反射ClassLoader类,找到里面的pathList变量,然后找到dexElements[]数组,该数组在修复之前只有两个元素,分别是classes.dex和classes2.dex(出错的),假设值数组1;
2.接着我们要往dexElements[]数组中添加classes2.dex文件。
Android中要想实现加载dex文件,需要使用DexClassLoader类加载classes2.dex(补丁),加载到dexElements[]数组中去,假设值数组2。
3.最后,我们需要把两个dexElements[]数组合并,作为一个新数组dexElements[],该数组中包含元素为classes2.dex(补丁),classes.dex和classes2.dex(出错的),完成后将数组返回赋值给系统的ClassLoader。
1.将下载的修复补丁拷贝到应用的内部缓存目录中
在上一篇文章中,我们已经生成了用于修复Bug的classes2.dex补丁包,通常我们会在APP后台子线程中自动调用热修复接口,并下载修复补丁,这里为了方便演示,我们把已经下载好的dex补丁文件放到SD卡中,然后将下载的修复补丁拷贝到应用的内部缓存目录中cacheDir,之所以这样做是因为下一步我们需要使用类加载器ClassLoader在内部缓存中加载classese.dex包。下面是我写的一个将classes2.dex包拷贝到内部缓存目录中的方法。/** * 修复方法 */ private void castielFixMethod() { // 创建一个内部缓存目录,把我们SD卡中的"classes2.dex"文件拷贝到内部缓存目录中cache File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE); String name = "classes2.dex"; String filePath = fileSDir.getAbsolutePath() + File.separator + name; File file = new File(filePath); if (file.exists()) {// 判断是否已经存在dex文件 Log.i("WY", "已经存在dex文件"); file.delete(); } // 通过IO流将dex文件写到我们的缓存目录中去 InputStream is = null; FileOutputStream fos = null; // 版权所有,未经许可请勿转载:猴子搬来的救兵http://blog.csdn.net/mynameishuangshuai try { is = new FileInputStream(Environment.getExternalStorageDirectory()); fos = new FileOutputStream(filePath); int len = 0; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } File f = new File(filePath); Log.i("WY", "filePath:" + f.getAbsolutePath()); if (f.exists()) { Toast.makeText(this, "新的dex文件已经覆盖", Toast.LENGTH_LONG).show(); } // 动态加载修复dex包 FixDexUtils.loadFixedDex(this); } catch (IOException e) { e.printStackTrace(); } finally { try { fos.close(); is.close(); } catch (IOException e) { e.printStackTrace(); } } }
2.实现热修复工具类
这里首先给大家普及一下类加载的原理:在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
ClassLoader去加载Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由于apk是一个类似于压缩包的东西,Android其实是使用一个优化的临时缓存目录optimizeDir(dex),专门把Dex文件解压进去,这样以后就从这个临时缓存目录中加载,提高效率。
在1代码中我们提到了loadFixedDex()方法,便是我们的核心热修复工具类,我给大家具体讲一下:
ClassLoader有一个简单的实现类-PathClassLoader。该类作为Android的默认的类加载器,本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
每个ClassLoader有一个pathList变量,是标识dex文件的路径,我们通过该路径加载dex文件,默认不分包的时候只有一个dex文件,当然谷歌在顶层设计时允许我们有多个dex文件。
ClassLoader去找optimizeDir(dex)目录,然后把目录添加到pathList里面去,接着去找目录下面的所有的dex文件,把这些dex文件当做一个数组放到dexElements中去,这样就可以有多个dex文件。
pathList{ dexElements{ [classes.dex,classes2.dex] } }
ClassLoader每加载一个类,它会先找classes.dex,如果找不到就去classes2.dex中找,如果里面又一个dex有问题,比如说classes2.dex出问题了,我们就需要弄一个修复的新的classes2.dex文件放到数组中去,替换掉有问题的;但是classes2.dex中可能有多个类,除了有问题的类,也可能有很多正确的类,我们在替换时没必要把所有的类都替换掉,所以我们只要替换有问题的类。
为此,我们可以采用一个策略,把新的替换的dex文件放到数组的最前面,最终数组的形态为:
[classes2.dex,classes.dex,classes2.dex]
这里解释下,ClassLoader类加载器先加载我们修复的正确的dex文件,然后顺序加载数组中其他的dex元素,到了最后加载到旧的classes2.dex元素,由于前面已经加载了更新的classes2.dex(更新的dex文件中只包含修复的class),那么旧的classes2.dex元素中的有Bug的class就不会再加载,而是只加载其余的没有错误的class。
整个流程其实非常简单,但是如果我们要实现这个过程却有个障碍,那就是由于我们的APK程序可能正在运行,谷歌并没有提供相关的接口方法去实现这一步骤,为此,我们需要使用反射的手段去实现。
1.首先需要反射ClassLoader类,找到里面的pathList变量,然后找到dexElements[]数组,该数组在修复之前只有两个元素,分别是classes.dex和classes2.dex(出错的),假设值数组1;
2.接着我们要往dexElements[]数组中添加classes2.dex文件。
Android中要想实现加载dex文件,需要使用DexClassLoader类加载classes2.dex(补丁),加载到dexElements[]数组中去,假设值数组2。
3.最后,我们需要把两个dexElements[]数组合并,作为一个新数组dexElements[],该数组中包含元素为classes2.dex(补丁),classes.dex和classes2.dex(出错的),完成后将数组返回赋值给系统的ClassLoader。
最后贴出热修复工具类源码
public class FixDexUtils { private static HashSet<File> loadedDex = new HashSet<File>(); public static void loadFixedDex(Context context) { if (context == null) { return; } // 首先拿到缓存目录 File fileSDir = context.getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE); File[] listFils = fileSDir.listFiles(); // 遍历缓存文件 for (File file : listFils) { // 如果文件是以"classes"开始或者以".dex"结尾,说明这是从SDK中拷贝回来的修复包 if (file.getName().startsWith("classes") || file.getName().endsWith(".dex")) { Log.i("WY", "当前dexName:" + file.getName()); loadedDex.add(file); } } doDexInject(context, fileSDir); } private static void doDexInject(Context context, File fileDir) { if (Build.VERSION.SDK_INT >= 23) { Log.i("WY", "Unable to do dex inject on SDK" + Build.VERSION.SDK_INT); } // .dex 的加载需要一个临时目录 String optimizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex"; File fopt = new File(optimizeDir); if (!fopt.exists()) fopt.mkdirs(); try { // 根据.dex 文件创建对应的DexClassLoader 类 for (File file : loadedDex) {// 循环迭代,用于多个修复包同时注入 DexClassLoader classLoader = new DexClassLoader( file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader()); // 注入 inject(classLoader, context); } } catch (Exception e) { e.printStackTrace(); } } private static void inject(DexClassLoader classLoader, Context context) { // 获取到系统的DexClassLoader 类 PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader(); try { Object dexElements = combineArray( getDexElements(getPathList(classLoader)), getDexElements(getPathList(pathLoader))); Object pathList = getPathList(pathLoader); setField(pathList, pathList.getClass(), "dexElements", dexElements); } catch (Exception e) { e.printStackTrace(); } } /** * 通过反射获取DexPathList中dexElements */ private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { return getField(paramObject, paramObject.getClass(), "dexElements"); } /** * 通过反射获取BaseDexClassLoader中的PathList对象 */ private static Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } /** * 通过反射获取指定字段的值 */ private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } /** * 通过反射设置字段值 */ private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj, value); } /** * 合并两个数组 */ private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); int j = i + Array.getLength(arrayRhs); 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; } }
相关文章推荐
- Android热修复(Hot Fix)案例全剖析(一)
- Android热修复(Hot Fix)案例全剖析(二)
- Android热修复(Hot Fix)案例全剖析(一)
- [Android 泥水匠] Android基础 之一:浅谈Android架构到HelloWorld案例的剖析
- Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!
- [MiniPro]Android GPS-从上到下整个框架剖析与案例解说
- Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!
- Android中热修复框架AndFix原理解析及案例使用
- Android anfix热修复 原理剖析
- [Android 泥水匠] Android基础 之一:浅谈Android架构到HelloWorld案例的剖析
- AIDL/IPC Android AIDL/IPC 进程通信机制——超具体解说及使用方法案例剖析(播放器)
- Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!
- Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!
- AIDL/IPC Android AIDL/IPC 进程通信机制——超详细讲解及用法案例剖析(播放器)
- AIDL/IPC Android AIDL/IPC 进程通信机制——超详细讲解及用法案例剖析(播放器)
- Android实例剖析笔记(三)
- 田志刚:知识管理六大失败案例剖析
- Anatomy of an Android Application - 剖析Android應用程式的四大區塊
- Android实例剖析笔记(二)
- Android实例剖析笔记(三)