一步步手动实现热修复Class文件的替换

Posted Sahadev_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一步步手动实现热修复Class文件的替换相关的知识,希望对你有一定的参考价值。

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

本节课程主要分为3块:

本节示例所用到的任何资源都已开源,项目中包含工程中所用到代码、示例图片、说明文档。项目地址为:
https://code.csdn.net/u011064099/sahadevhotfix/tree/master


在上一节了解了基本的类加载原理之后,我们这一节开始对工程内部的类实行替换。

Tips: 本章主要依赖文章http://blog.csdn.net/vurtne_ye/article/details/39666381中的未实现代码实现,实现思路也源自该文章,在阅读本文之前可以先行了解。

这一节我们主要实现的流程有:

  • 在工程内创建相同的ClassStudent类,但在调用getName()方法返回字符串时会稍有区别,用于结果验证
  • 使用DexClassLoader加载外部的user.dex
  • 将DexClassLoader中的dexElements放在PathClassLoader的dexElements之前
  • 验证替换结果

创建工程内的ClassStudent

我们在第一节中演示了如何加载外部的Class,为了起到热修复效果,那么我们需要在工程内有一个被替换的类,被替换的ClassStudent类内容如下:

package com.sahadev.bean;

/**
 * Created by shangbin on 2016/11/24.
 * Email: sahadev@foxmail.com
 */

public class ClassStudent {
    private String name;

    public ClassStudent() {

    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name + ".Miss";
    }

}

外部的ClassStudent类的内容如下:

package com.sahadev.bean;

/**
 * Created by shangbin on 2016/11/24.
 * Email: sahadev@foxmail.com
 */

public class ClassStudent {
    private String name;

    public ClassStudent() {

    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name + ".Mr";   
    }
}

这两个类除了在getName()方法返回之处有差别之外,其它地方一模一样,不过这足可以让我们说明情况。

我们这里要实现的目的: 我们默认调用getName()方法返回的是“xxxx.Miss”,如果热修复成功,那么再使用该方法的话,返回的则会是“xxxx.Mr”

对含有包名的类再次编译

因为第一节中专门声明了不可以对类声明包名,但是这样在android工程中无法引用到该类,所以把不能声明包名的问题解决了一下。

不能声明包名的主要原因是在编译Java文件时,没有正确的使用命令。对含有包名的Java文件应当使用以下命令:

javac -d ./ ClassStudent.java

经过上面命令编译后的.class文件便可以顺利通过dx工具的转换。

我们还是按照第一节的步骤将转换后的user.dex文件放入工程中并写入本地磁盘,以便稍后使用。

Dex转换注意: 很多同学走到这一步时遇到了问题,这是因为第一章介绍的时候并没有带包名,所以在这里我们所使用的命令的最后一个参数应当携带包的相对路径,例如:

dx –dex –output=user.dex .\\com\\sahadev\\bean\\ClassStudent.class

替换工程内的类文件

在开始之前还是再回顾一下实现思路:类在使用之前必须要经过加载器的加载才能够使用,在加载类时会调用自身的findClass()方法进行查找。然而在Android中类的查找使用的是BaseDexClassLoader,BaseDexClassLoader对findClass()方法进行了重写:

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);

        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }

        return clazz;
    }

pathList是类DexPathList的实例,这里pathList.findClass的实现如下:

    public 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;
    }

由此我们可以得知类的查找是通过遍历dexElements来进行查找的。所以为了实现替换效果,我们需要将DexClassLoader中的Element对象放到dexElements数组的第0个位置,这样才能在BaseDexClassLoader查找类时先找到DexClassLoader所用的user.dex中的类。

Tips: 如果对上面这段内容看不懂的,没关系,可以移步到本系列课程的第二节了解一下类加载的具体流程。

类的加载是从上而下加载的,所以就算是DexClassLoader加载了外部的类,但是在系统使用类的时候还是会先在ClassLoader中查找,如果找不到则会在BaseDexClassLoader中查找,如果再找不到,就会进入PathClassLoader中查找,最后才会使用DexClassLoader进行查找,所以按照这个流程外部类是无法正常发挥作用的。所以我们的目的就是在查找工程内的类之前,先让加载器去外部的dex中查找。

好了,再次梳理了思路之后,我们接下来对思路进行实践。

下面的方法是我们主要的注入方法:

    public String inject(String apkPath) {
        boolean hasBaseDexClassLoader = true;

        File file = new File(apkPath);
        try {
            Class.forName("dalvik.system.BaseDexClassLoader");
        } catch (ClassNotFoundException e) {
            hasBaseDexClassLoader = false;
        }
        if (hasBaseDexClassLoader) {
            PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", pathClassLoader);
            try {
                Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
                Object pathList = getPathList(pathClassLoader);
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
                return "SUCCESS";
            } catch (Throwable e) {
                e.printStackTrace();
                return android.util.Log.getStackTraceString(e);
            }
        }
        return "SUCCESS";
    }

Tips: 这段代码原封不动采用于http://blog.csdn.net/vurtne_ye/article/details/39666381文章中最后的实现代码,但是该文章并没有给出具体的注入细节。我们接下里的过程就是对没有给全的细节进行补充与讲解。

这段代码的核心在于将DexClassLoader中的dexElements与PathClassLoader中的dexElements进行合并,然后将合并后的dexElements替换原先的dexElements。最后我们在使用ClassStudent类的时候便可以直接使用外部的ClassStudent,而不会再加载默认的ClassStudent类。

首先我们通过classLoader获取各自的pathList对象:

public Object getPathList(BaseDexClassLoader classLoader) {
        Class<? extends BaseDexClassLoader> aClass = classLoader.getClass();

        Class<?> superclass = aClass.getSuperclass();
        try {

            Field pathListField = superclass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object object = pathListField.get(classLoader);

            return object;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
}

在使用以上反射的时候要注意,pathList属性属于基类BaseDexClassLoader。所以如果直接获取DexClassLoader或者PathClassLoader的pathList属性的话,会得到null。

其次是获取pathList对应的dexElements,这里要注意dexElements是个数组对象:

    public Object getDexElements(Object object) {
        if (object == null)
            return null;

        Class<?> aClass = object.getClass();
        try {
            Field dexElements = aClass.getDeclaredField("dexElements");
            dexElements.setAccessible(true);
            return dexElements.get(object);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;

    }

接下来我们将两个数组对象合并成为一个:

    public Object combineArray(Object object, Object object2) {
        Class<?> aClass = Array.get(object, 0).getClass();

        Object obj = Array.newInstance(aClass, 2);

        Array.set(obj, 0, Array.get(object2, 0));
        Array.set(obj, 1, Array.get(object, 0));

        return obj;
    }

上面这段代码我们根据数组对象的类型创建了一个新的大小为2的新数组,并将两个数组的第一个元素取出,将代表外部dex的dexElement放在了第0个位置。这样便可以确保在查找类时优先从外部的dex中查找。

最后将原先的dexElements覆盖:

    public void setField(Object pathList, Class aClass, String fieldName, Object fieldValue) {

        try {
            Field declaredField = aClass.getDeclaredField(fieldName);
            declaredField.setAccessible(true);
            declaredField.set(pathList, fieldValue);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

验证替换结果

好,我们做完以上的工作之后,写一段代码来进行验证:

    /**
     * 验证替换类后的效果
     */
    private void demonstrationRawMode() {
        ClassStudent classStudent = new ClassStudent();
        classStudent.setName("Lavon");
        mLog.i(TAG, classStudent.getName());
    }

如果我们没有替换成功的话,那么这里默认使用的是内部的ClassStudent,getName()返回的会是Lavon.Miss
如果我们替换成功的话,那么这里默认使用的是外部的ClassStudent,getName()返回的则会是Lavon.Mr

我们实际运行看下效果:
这里写图片描述

这说明我们已经完成了基本的热修复。有任何疑问欢迎留言。


我建了一个QQ群,欢迎对学习有兴趣的同学加入。我们可以一起探讨、深究、掌握那些我们会用到的技术,让自己不至于太落伍。
这里写图片描述

以上是关于一步步手动实现热修复Class文件的替换的主要内容,如果未能解决你的问题,请参考以下文章

一步步手动实现热修复-类的加载机制简要介绍

Android热修复原理

Android主要热更新技术原理

基于cydia Hook在线热修复补丁方案

Android热修复与插件化实践之路

Android 使用类加载器原理实现热修复