美文网首页
Android中的dex插桩热修复(类文件)

Android中的dex插桩热修复(类文件)

作者: Android_Gleam | 来源:发表于2020-09-05 01:14 被阅读0次

这篇文章继续我们之前写的Classloader,来说一下Android中dex插桩方式实现热修复的方案。
Android中的ClassLoader

1.实现原理

我们程序中的类在被加载的时候,最终会调用到DexPathList的findClass方法,然后在该方法中遍历Element[],获取到class返回。

我们从代码中可以得知,如果在第一个Element中找到了class,后面的就不会处理了,所以我们就应该想办法,把我们修复过的dex文件转换成Element,然后将这个Element放到的Element[]的前面,这样在加载类的时候,就会优先调用我们修复过的class,从而达到修复bug的目的。

public Class<?> findClass(String name, List<Throwable> 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;
    }

2.实现方案

首先明确我们的目的,就是获取到Element[],然后把我们修复的dex转换为Element,再添加到Element[]的前面。

(1)Element[]在哪?
在DexPathList

(2)那DexPathList在哪?
在BaseDexClassLoader

(3)我们如何获取BaseDexClassLoader?
我们知道,我们自己的应用程序中的类是通过PathClassLoader加载,而PathClassLoader是 BaseDexClassLoader的子类,所以我们可以通过PathClassLoader获取BaseDexClassLoader,然后一步一步向上去获取,或者直接根据全类名直接反射获取。

明确了我们的方向后,我们开始按步骤实现。

2.1获取ClassLoader

首先我们获取到ClassLoader,这就很简单了 直接调用getClass().getClassLoader()就可以获取到了,或者直接通过Application、Activity等直接getClassLoader()也可以。这里我们获取到的是PathClassLoader。

2.2反射获取到DexPathList属性对象pathList

这里要注意的是,我们获取到的是PathClassLoader,而DexPathList对象是在PathClassLoader的父类(BaseDexClassLoader)中,所以我们反射调用的时候要注意,DexPathList对象BaseDexClassLoader
中的变量名是pathList,我们反射获取的时候要用到。

2.3.把补丁包patch.dex转化为Element[]

现在的问题就是我们应该如何把我们的dex文件转换成Element,我们可以通过两种方式来完成。

2.3.1通过makeDexElements方法

我们首先看PathClassLoader,它的构造方法中需要传入一个dexPath的字段,这个就是dex的路径。

public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

然后我们继续跟进到它的父类中查看,也就是BaseDexClassLoader

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }

我们发现它将DexPathList传给了DexPathList,我们继续跟进到DexPathList。

DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
       ...
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
      ...
    }

最终我们发现是将dexPath传给了makeDexElements这个方法,而这个方法的返回值就是一个Element[],说明它就是我们最终需要的将dex转为Element的方法。

这里我们可以看到有一个splitDexPath方法,因为我们的dex路径不可能是一个,是按照一定规则拼接后传入的,splitDexPath就是将路径进行分割(应该是按照:分割),然后转换List<File>返回。

然后我们看makeDexElements的源码

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
    ...      
}

files就是dex转换来的List<File>
optimizedDirectory参数是优化dex的输出路径,真正加载dex之前,会进行odex 优化。
suppressedExceptions是一个List<IOException> ,源码中直接new了一个然后传进来,我们外面调用的时候也按照这种方式。
loader是一个ClassLoader类型,我们可以直接传入我们上面获取的PathClassLoader。
isTrusted这个翻译过来就是信任的,但是我看源码中传入的都是false,这里我们也传入false。

这里在写文章阅读源码的时候发现,不同的版本makeDexElements方法的参数不一样,学习的时候用的是老版本的,没有最后的两个参数,但是28的版本中是有这两个参数的,所以可能描述和使用上有不对的地方,大家可以指出来,感激不尽,下面我们看下另外一种获取Element[]的方法。

2.3.2通过makeDexElements方法

这种方法就直接贴代码了,具体详细的代码在文末会贴出。

 DexClassLoader dexClassLoader = new DexClassLoader(patch, application.getFilesDir().getAbsolutePath(),
                    null, classLoader);
            Object pluginPathList = patchField.get(dexClassLoader);
            Object[] patchElements = (Object[]) dexElements.get(pluginPathList);

2.4.把补丁包patch.dex转化的Element[]添加到系统的Element[]中(合并)

我们在通过反射获取到DexPathList中的dexElements就可以获取到系统的Element[],然后将系统的Element[]和我们修复文件的Element[]二者合并,这样就ok了。

代码很简单 我就直接贴在下面了。

修复方法

/**
     * @param application 上下文  这里也可以直接传入classloader
     *                    因为一般我们都是在application中调用修复 所以这里写的application
     * @param patch       补丁包路径
     */
    public static void installPatch(Application application, String patch) {
        //补丁包文件
        File patchFile = new File(patch);
        if (!patchFile.exists()) {
            return;
        }

        try {
            //1.获取当前应用的PathClassLoader
            ClassLoader classLoader = application.getClassLoader();
            //2.反射获取到DexPathList属性对象pathList
            Field patchField = ShareReflectUtil.findField(classLoader, "pathList");
            Object pathList = patchField.get(classLoader);

            //3.1 获取pathList的dexElements属性(old)
            Field dexElements = ShareReflectUtil.findField(pathList, "dexElements");
            Object[] oldElements = (Object[]) dexElements.get(pathList);

            //3.2 把补丁包patch.dex转化为Element[] (patch) 3.2.1和3.2.2任选其一
            //3.2.1 通过makePathElements方法
//            Method makePathElements = ShareReflectUtil.getMethod(pathList, "makePathElements", List.class, File.class, List.class,ClassLoader.class,boolean.class);
//            List<File> files = new ArrayList<>();
//            files.add(patchFile);
//            File fileDir = application.getFilesDir();
//            ArrayList<Exception> suppressedExceptions = new ArrayList<>();
//            //如果是调用静态方法 pathList可以传null 否则必须传入对应对象
//            Object[] patchElements = (Object[]) makePathElements.invoke(pathList, files, fileDir, suppressedExceptions,classLoader,false);

            //3.2.2 通过DexClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(patch, application.getFilesDir().getAbsolutePath(),
                    null, classLoader);
            Object pluginPathList = patchField.get(dexClassLoader);
            Object[] patchElements = (Object[]) dexElements.get(pluginPathList);

            //3.3 patch-old合并,并反射设置给pathList的dexElements
            Object[] newElements = (Object[]) Array.newInstance(patchElements.getClass().getComponentType(),patchElements.length + oldElements.length);
            System.arraycopy(patchElements,0,newElements,0,patchElements.length);
            System.arraycopy(oldElements,0,newElements,patchElements.length,oldElements.length);
            dexElements.set(pathList,newElements);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

反射工具类

public class ShareReflectUtil {

    /**
     * @param instance 对象
     * @param name     属性名
     * @return
     */
    public static Field findField(Object instance, String name) {
        Class<?> cls = instance.getClass();

        while (cls != Object.class) {
            try {
                Field field = cls.getDeclaredField(name);
                if (field != null) {
                    field.setAccessible(true);
                    return field;
                }

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

            cls = cls.getSuperclass();
        }
        throw new RuntimeException("no source field" + name);
    }

    public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) {
        Class<?> cls = instance.getClass();

        while (cls != Object.class) {
            try {
                Method method = cls.getDeclaredMethod(name,parameterTypes);
                if (method != null) {
                    method.setAccessible(true);
                    return method;
                }

            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }

            cls = cls.getSuperclass();
        }
        throw new RuntimeException("no source method" + name);
    }
}

总结:

其实原理很简单,因为findClass方法默认是从缓存中寻找,如果缓存中没有,再去Element[]查找,因为我们将修复的dex文件插入到系统读取的Element[]数组的前面,就只会加载我们修复后的代码了。
而且初次加载后,就会加入到缓存中,之后在加载也不会在重新获取了,所以dex插桩方式进行的修复,不能即时生效,只能重启应用后才能生效,因为加载过的类不会重新在加载了。

相关文章

网友评论

      本文标题:Android中的dex插桩热修复(类文件)

      本文链接:https://www.haomeiwen.com/subject/mawtektx.html