手写Android热修复

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/lmq121210/article/details/101615108

其实关于为什么会产生热修复,热修复怎么用,这些网上一大篇博客,本篇博客的内容是 :手写一个Android热修复Demo

要手写Demo ,首先需要掌握 两点:反射和ClassLoader。关于ClassLoader,不了解的可以看下上篇博客 Android中的类加载器——ClassLoader
反射知识,可以看下这个博客:Android 插件化开发——基础底层知识(反射), 这里直接贴出我们Demo 中需要用到的反射工具类:


/**
 * FileName:SharedReflectUtils
 * Create By:liumengqiang
 * Description:TODO
 */
public class SharedReflectUtils {

    /**
     * 反射获取一个属性
     *
     * @param instance PathClasLoader
     * @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);
                //设置权限
                declaredField.setAccessible(true);
                return declaredField;
            } catch (NoSuchFieldException e) {
            }
        }
        throw new NoSuchFieldException("Field:" + name + " not found in " + instance.getClass());
    }


    /**
     * 反射获取一个方法
     *
     * @param instance PathClasLoader
     * @param name
     * @return
     */
    public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) throws NoSuchMethodException {
        for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) {
            try {
                Method declaredMethod = cls.getDeclaredMethod(name,parameterTypes);
                //设置权限
                declaredMethod.setAccessible(true);
                return declaredMethod;
            } catch (NoSuchMethodException e) {
            }
        }
        throw new NoSuchMethodException("Method:" + name + " not found in " + instance.getClass());
    }
}

这个工具类 的作用是:递归自己以及自己的父类查找方法或者属性,如果找到 就返回。

热修复核心原理

其实说到热修复原理,上一篇博客,说到Element的时候,我们捎带提了一下,现在重新 捋一下思路:
loadClass查找类的时候 ,不论是PathClassLoader还是DexClassLoader都会走其父类的BaseDexClassLoader构造方法,而在其构造方法内创建了一个变量:pathList:

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        //初始化pathList
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }

而真正调用loadClass的时候,会执行抽象类ClassLoader.java的loadClass的方法,先查找缓存,再从查找父亲,最后再调用自己的findClass方法查找,而在ClassLoader中,findClass方法里面 就抛出了个异常,把实现交给了子类,所以在BaseDexClassLoader中,看下findClass方法:

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        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;
    }

可以看到,实际上调用的 是pathList的findClass方法,在这个方法内,维护了个Element数组,里面 存储的 是DexFile文件,我们的APk打包的时候会生成dex文件,而APK安装的时候,会将APK中的dex文件优化转换成dexopt, 而Element 中存储的就是经过优化后的文件:DexFile,

    public Class<?> findClass(String name, List<Throwable> suppressed) {
    	//遍历dexElements,
        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;
    }

遍历Element数组,从前往后遍历,如果找到了class,那么就直接返回,不管之后是否还存在第二个相同的class。如下图所示:
在这里插入图片描述
那么,此时我们可以猜想,假如我们的APK中的a文件出了问题,那么我们可以修改a文件,然后打成补丁包patch .dex,放入到Element的最前面,那么当APP运行的时候,从前往后查找,这样的话就先查找我们的额patch.dex文件,那么就会找到a.class ,这样的话就直接返回,不再继续查找。这样我们的BUG就修复了,因为获得到的是修复之后的a.class!

怎样把生成的补丁包插入到Element数组中?

关于这个问题,在捋一下loadClass流程

  1. 首先通过递归反射获取到pathList变量,
  2. 然后在通过反射获取到pathList中的Element数组
  3. 将我们的 patch.dex 或者 path.jar通过反射调用pathList的makeDexElements方法,获取到新的Element数组
  4. 将新老数组Element合并成一个新的Element数组replaceArray
  5. 在将数组replaceArray塞回到pathList对象中。

好,总体流程就是上面的,那么可能有的小伙伴有疑问了:makeDexElements方法是什么鬼?

 public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {

     ........

        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);


       .......
    }

可以看到通过调用makeDexElements方法创建了dexElements对象(也就是Element数组)。

注意:Android6.0之前是:makePathElements ; 而Android6.0之前是:makeDexElements方法创建Element数组,因此 我们创建Element的时候,要不同Android版本,不同处理!

开始手撸代码

首先我们需要创建一个加载补丁包patch.jar的工具类:

   /**
     * 执行修复BUG
     */
    private  static void installPatch23(Context context, String  path) {
        //获取私有目录缓存
        File cacheDir = context.getCacheDir();
        ClassLoader classLoader = context.getClassLoader();
        try{
            //获取ClassLoader的pathList属性
            Field pathListField = SharedReflectUtils.getField(classLoader, "pathList");
            Object pathList = pathListField.get(classLoader);
            //获取pathList 中的dexElements数组
            Field dexElementsField = SharedReflectUtils.getField(pathList, "dexElements");
            Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);


            Method makePathElementsMethod = SharedReflectUtils.getMethod(pathList, "makePathElements", List.class,  File.class,  List.class);
            ArrayList<File> files = new ArrayList<>();
            files.add(new File(path));
            //把新的补丁包通过makePathElementsMethod方法转化成Element数组
            Object[] newDexElements = (Object[]) makePathElementsMethod.invoke(null, files, cacheDir, new ArrayList<IOException>());

            //创建新的数组
            Object[] replaceElements = (Object[]) Array.newInstance(oldDexElements.getClass().getComponentType(), oldDexElements.length + newDexElements.length);

            //拷贝补丁包 数组和老数组,复制到新数组中,其中补丁包 数组要插入到新数组的 第零个
            System.arraycopy(newDexElements, 0, replaceElements, 0, newDexElements.length);
            System.arraycopy(oldDexElements, 0, replaceElements, newDexElements.length,  oldDexElements.length);

            //重新设置pathList属性中的Elements数组
            dexElementsField.set(pathList, replaceElements);

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

可以看到上述的代码顺序主要就是上一节分析的步骤。至此我们的 加载补丁包的方法已经写好了。我们什么时候加载 补丁包呢? 答案是在自定义Application的attachBaseContext中初始化的,因为attachBaseContext方法执行的时间比onCreate早 :

public class BaseApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        EnjoyFix.installPatch23(this, "/sdcard/patch.jar");
    }
}

注意:这里我把补丁包放在sdcard根目录了,当然了这个目录可以自己定义。

还有一点就是要注意打开存储权限!!!

然后创建一个测试类 :

/**
 * FileName:Test
 * Create By:liumengqiang
 * Description:TODO
 */
public class Test {
    private static final String TAG = "Test";
    public static  int  test() {
        return 1/0;
    }
}

然后我们在MainActivity中调用Test的test方法:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Test.test();
    }
}

这样的话,然后 运行,看一下, 崩溃了:

在这里插入图片描述

这样的话 我们修改test方法:

/**
 * FileName:Test
 * Create By:liumengqiang
 * Description:TODO
 */
public class Test {
    private static final String TAG = "Test";
    public static  int  test() {
        return 1;
    }
}

这样的话代码 就可以了,然后使用点击Build -> make module ‘app’, 用于编译生成 class文件 。然后我们使用

dx --dex --output=output.dex  input.class

生成补丁包 ,
dx: 我们的SDK目录下的文件:比如我的是:
C:\Users\mayn\AppData\Local\Android\Sdk\build-tools\28.0.3\dx
** --dex** 制定是生成dex文件
output.dex: 输出的补丁包名称
input.class 就是你要生成补丁包的 class类文件 或者目录,
例如:我这里执行的是:

C:\Users\mayn\AppData\Local\Android\Sdk\build-tools\28.0.3\dx --dex --output = patch.jar  com/bitcoin/juwan/androidfixproject/Test.class

然后拷贝patch.jar 放入 /sdcard/目录下 , 此时执行我们的app:

在这里插入图片描述
修复成功!

至此手写热修复就算完成了,文中如有错误,欢迎指正。

总结:实际上热修复的核心原理 就是ClassLoader加载机制,通过替换Element数组,达到修复BUG的目的。

猜你喜欢

转载自blog.csdn.net/lmq121210/article/details/101615108