最近一直在阅读微信Tinker的源码,为了加深理解和记忆,便自己动手实现了一下热修复,做了一个比较简单的demo,并写了这篇博客与大家分析,理解有误的地方还请大家指出~
阅读本文大概需要5分钟时间。

类加载器

说到热修复,那么肯定要先说一下Android中的类加载器,这里我们简单介绍一下。
在Android中,我们应用自己的Class是交由PathClassLoader来加载的,PathClassLoader的代码很简单,只有两个构造函数

public class PathClassLoader extends BaseDexClassLoader {    public PathClassLoader(String dexPath, ClassLoader parent) {        super(dexPath, null, null, parent);    }    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {        super(dexPath, null, librarySearchPath, parent);    }}

我们看到他的父类其实是BaseDexClassLoader,那么奥妙一定是在这里面,我们看一下BaseDexClassLoader的源码

public class BaseDexClassLoader extends ClassLoader {    private final DexPathList pathList;    ...    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        List suppressedExceptions = new ArrayList();        Class c = pathList.findClass(name, suppressedExceptions);        if (c == null) {            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);            for (Throwable t : suppressedExceptions) {                cnfe.addSuppressed(t);            }            throw cnfe;        }        return c;    }    ...}

这里我们可以看到findClass查找Class的方法,BaseDexClassLoader重写了ClassLoaderfindClass方法,在方法内部调用了DexPathListfindClass方法,我们进去看看这个方法做了什么

final class DexPathList {    /**     * List of dex/resource (class path) elements.     * Should be called pathElements, but the Facebook app uses reflection     * to modify 'dexElements' (http://b/7726934).     */    private Element[] dexElements;    ...      public Class<?> findClass(String name, List suppressed) {        for (Element element : dexElements) {            Class<?> clazz = element.findClass(name, definingContext, suppressed);            if (clazz != null) {                return clazz;            }        }        if (dexElementsSuppressedExceptions != null) {            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));        }        return null;    }    ...}

可以看到,DexPathList中有一个Element数组的成员变量,根据注释可以知道这个数组是用于存放dex的路径节点的,通过它的findClass方法可以知道,通过遍历数组然后找到需要的类并返回,我们的切入点便是这里,将我们需要进行热修复的“补丁”插入到这个数组的前端,那么在寻找Class的时候便会先找到我们已经修复的类,从而实现热修复。

为了控制本文篇幅,这里就不对Android的类加载器进行太多讲解,对这一块还不是特别清楚的童鞋可以先看看鸿洋大神前阵子推送的jjlanbupt写的《Android插件化框架系列之类加载器》。

动手实践

Demo

这里比较简单,就是点击SHOW按钮弹出个Toast,我们现在要做的就是通过热修复把Toast中的文本进行替换。SHOW按钮的点击事件如下

  @Override    public void onClick(View v) {        int viewId = v.getId();        ...        if (viewId == R.id.btn_show) {            Toast.makeText(this, Test.test(), Toast.LENGTH_SHORT).show();        }    }

这里我们看一下Test类,也是比较简单

public class Test {    public static String test() {        return "hello world";    }}

ok,目标明确,我们要通过热修复更改test方法中返回的字符串,开始撸代码

public class Test {    public static String test() {        return "I am change!!!";    }}

首先,我们把字符串改了之后把它编译为class,再打包成jar,如果忘了如何把java编译后打包成jar的方法的同学,可以网上搜一下,这里就不再啰嗦了。
打包了jar后,别忘记要使用sdk中的dx工具将jar包转换成dx格式的jar包,工具目录在sdk目录下的**...\build-tools\ **中


在控制台使用该命令即可,现在我们已经完成了补丁包的制作,我们先来看一下执行热修复的代码

private static void patch(Context base) {        try {            //获取PathClassLoader加载器            ClassLoader classLoader = MainActivity.class.getClassLoader();            //反射获得BaseDexClassLoader中的pathList成员变量            Field dexPathListField = classLoader.getClass().getSuperclass().getDeclaredField("pathList");            //设为可访问            dexPathListField.setAccessible(true);            //获得PathClassLoader中的pathList对象            Object pathList = dexPathListField.get(classLoader);            //反射获得DexPathList中的dexElements成员变量            Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");            //设为可访问            dexElementsField.setAccessible(true);            //获得pathList中的dexElements数组对象            Object dexElements[] = (Object[]) dexElementsField.get(pathList);            //反射获得pathList中的makeDexElements方法            Method method= pathList.getClass().getDeclaredMethod("makeDexElements", ArrayList.class, File.class,                    ArrayList.class);            //设为可访问            method.setAccessible(true);            List files = new ArrayList<>();            ArrayList suppressedExceptions = new ArrayList();            //获得我们的patch补丁包            File patchFile = new File(copyAssetsFile("patch.jar", base));            files.add(patchFile);            //指定解压目录            File optimizedDirectory = new File(base.getFilesDir().getAbsolutePath() + File.separator + "patch");            if (!optimizedDirectory.exists()) {                optimizedDirectory.mkdirs();            }            //执行makeDexElements方法,解析我们的补丁包获得dexElements数组            Object dexElementsResult[] = (Object[]) method.invoke(pathList, files, optimizedDirectory, suppressedExceptions);            //创建一个新的数组            Object finalResult[] = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + dexElementsResult.length);            //先把我们的补丁包的dexElements数组放入刚创建的数组            System.arraycopy(dexElementsResult, 0, finalResult, 0, dexElementsResult.length);            //再把原来的dexElements数组放入            System.arraycopy(dexElements, 0, finalResult, dexElementsResult.length, dexElements.length);            //将新的数组设置回去            dexElementsField.set(pathList, finalResult);            for (Object o : finalResult) {                Log.d(MainActivity.class.getSimpleName(), o.toString());            }        } catch (NoSuchFieldException e) {            e.printStackTrace();        } catch (IllegalAccessException e) {            e.printStackTrace();        } catch (NoSuchMethodException e) {            e.printStackTrace();        } catch (InvocationTargetException e) {            e.printStackTrace();        }    }

这里主要就是使用反射获取到dexElements数组,然后将我们自己的补丁包插入。因为我这里偷了个懒把补丁包直接放在assets目录下,copyAssetsFile方法是将assets目录下的文件复制到我们的app目录下,因为PathClassLoader只能读取app目录下的文件,代码也很简单,这里也贴一下

private static String copyAssetsFile(String assetsFileName,Context context) {        String src = context.getFilesDir().getAbsolutePath() + File.separator + assetsFileName;        InputStream inputStream = null;        OutputStream outputStream = null;        try {            inputStream = context.getAssets().open(assetsFileName);            outputStream = new BufferedOutputStream(new FileOutputStream(src));            byte[] temp = new byte[1024];            int len;            while ((len = (inputStream.read(temp))) != -1) {                outputStream.write(temp, 0, len);            }        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } finally {            try {                if (inputStream != null) {                    inputStream.close();                }                if (outputStream != null) {                    outputStream.flush();                    outputStream.close();                }            } catch (IOException e) {                e.printStackTrace();            }        }        return src;    }

到这里我们代码就搞定了,但是有两个地方还是需要注意一下:

  1. 如果是在Activity中进行热修复,并且开启了instant run,那么会导致看不到热修复的效果,因为Android Studio的instant run会将ClassLoader替换成DelegateClassLoader,这里建议大家打包成apk然后提取出来安装运行就可以。
  2. 为了避免掉进CLASS_ISPREVERIFIED的坑中,这里我们使用Art的机型来运行(Android 5.0以上),具体原因大家可以参考安卓App热补丁动态修复技术介绍,微信Tinker由于采用了是全量替换dex的方法,所以也可以说是从另一个角度解决了问题。

最后我们看一下运行效果,重启app,先点击patch按钮,再点击show按钮

可以看到,patch成功。

总结

最近看Tinker的源码,确实对各种异常情况处理的很到位,收获颇丰。各位有兴趣的同学也可以去阅读一番,自己动手实践实践,确实能加深自己的理解。若本文中哪里错误的地方大家也可以在评论中指出,谢谢大家~

更多相关文章

  1. Android之Activity生命周期浅析(一)
  2. Android中获取控件的宽度以及高度的几种方法
  3. [置顶] Android:在任意位置获取应用程序Context
  4. Android(安卓)View相关-View的常用方法及使用区别
  5. Android(安卓)C调用Java
  6. Android(安卓)使用RxJava+Retrofit +Realm 组合加载数据 (二)
  7. android学习笔记1-android介绍以及学习方法
  8. android 常用的调试方法
  9. Android中的Context几种获取方法和区别

随机推荐

  1. Android(安卓)实用工具Hierarchy Viewer
  2. android打开系统联系人界面
  3. Android(安卓)安装环境搭建
  4. android之wifi移植全过程(二)
  5. 实验1:第一个Android程序
  6. Android之使用Android-query框架进行开发
  7. Android文件存储位置简述
  8. Android(安卓)SystemProperties (java)
  9. Android(安卓)7.0 给开发者带来了什么
  10. Android开发的未来发展方向