当前位置:   article > 正文

Android dex修复工具,Android 简单热修复(下)——基于DexClassLoader的实现

重写dexclassloader方法

前面Java类加载器的介绍中写过关于ClassLoader的基础知识,包括了双亲委派机制、自定义ClassLoader等内容。但是,前面讲到的都是基于JVM的内容,在这里需要清楚下:Android采用的Dalvik虚拟机(DVM)和ART虚拟机(4.4版本发布)。

简单描述Android采用的虚拟机和JVM的区别

送分题(敲黑板)!!

8527070bfe92

根据广大网友描述,区别如下:

Dalvik基于寄存器,而JVM基于栈。基于寄存器的虚拟机对于编译后变大的程序来说,在它们执行的时候,花费的时间更短。

JVM运行java字节码,DVM运行的是其专有的文件格式Dex。

ART与Dalvik最大的不同在于,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。

ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这就是“时间换空间大法”。

预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。

如果非要深究为什么上面的一定是对的?我只能说——我也不懂。。作为菜鸡,只能站在巨人的肩膀看世界了(虽然有的的确不靠谱)。

8527070bfe92

DVM执行Dex文件

上面讲过:JVM运行java字节码,DVM运行的是其专有的文件格式Dex。Dex文件是由java的.class文件通过Android Sdk的build-tools目录下的dx.bat生成,生成命令如下:

dx --dex --output=[outFilePath] [inputDirPath]

举个例子:

package com;

public class Main {

public static void main(String[] args) {

System.out.println("hello");

}

}

将Main.class文件和其目录拷贝到桌面(主要是为了方便),并执行下面的命令:

8527070bfe92

执行命令

这里面最后的输入路径需要注意下,输入路径需要是.class包名的上一级目录,否则生成Dex文件会报错。执行命令后会生成文件:

8527070bfe92

dex文件

接着,我们将dex文件放到/mnt/sdcard/目录下:

8527070bfe92

放到目录

通过命令adb shell dalvikvm -cp [dexFilePath] [className]执行:

8527070bfe92

执行结果

OK,DVM执行Dex文件的结果已经出来了。

Android的类加载器

上面已经说了DVM可以执行Dex文件,其实我们也可以知道不管采用什么虚拟机,还是需要将执行的代码(字节码)加载到内存,最终执行。我们先看下Android里的ClassLoader

8527070bfe92

image.png

Android的ClassLoader是PathClassLoader,需要源码的可以在这里搜索。

PathClassLoader是BaseDexClassLoader的子类,下面我们来看下源码:

PathClassLoader.java:

public class PathClassLoader extends BaseDexClassLoader {

// 调用了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.java:

public class BaseDexClassLoader extends ClassLoader {

private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory,

String librarySearchPath, ClassLoader parent) {

super(parent);

// 创建DexPathList对象

this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

......

}

public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {

super(parent);

this.pathList = new DexPathList(this, dexFiles);

}

// 重写了findClass方法,遵循了双亲委派机制

@Override

protected Class> findClass(String name) throws ClassNotFoundException {

List suppressedExceptions = new ArrayList();

// 调用pathList的findClass方法

Class c = pathList.findClass(name, suppressedExceptions);

// 找到了Class则return

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;

}

......

}

这两个类的源码并不是很多,主要逻辑还是在BaseDexClassLoader中。BaseDexClassLoader重写了findClass方法,遵循双亲委派机制,并且这里调用了BaseDexClassLoader的成员变量pathList的findClass方法。如果pathList.findClass方法找到了需要的Class,那么将结果返回。我们需要看下DexPathList的源码:

/*package*/ final class DexPathList {

private static final String DEX_SUFFIX = ".dex";

private static final String zipSeparator = "!/";

private final ClassLoader definingContext;

// 这个属性很重要,热修复的关键

private Element[] dexElements;

private final NativeLibraryElement[] nativeLibraryPathElements;

private final List nativeLibraryDirectories;

private final List systemNativeLibraryDirectories;

private IOException[] dexElementsSuppressedExceptions;

......

public DexPathList(ClassLoader definingContext, String dexPath,

String librarySearchPath, File optimizedDirectory) {

......

this.definingContext = definingContext;

ArrayList suppressedExceptions = new ArrayList();

// save dexPath for BaseDexClassLoader

// 根据传入的dex的路径生成Element数组

this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,

suppressedExceptions, definingContext);

......

}

......

private static Element[] makeDexElements(List files, File optimizedDirectory,

List suppressedExceptions, ClassLoader loader) {

Element[] elements = new Element[files.size()];

int elementsPos = 0;

for (File file : files) {

if (file.isDirectory()) {

// We support directories for looking up resources. Looking up resources in

// directories is useful for running libcore tests.

// 支持目录的形式

elements[elementsPos++] = new Element(file);

} else if (file.isFile()) {// 如果是文件的话

String name = file.getName();

// 文件名以.dex结尾

if (name.endsWith(DEX_SUFFIX)) {

// Raw dex file (not inside a zip/jar).

try {

// 创建dexFile对象

DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);

// 数组赋值

if (dex != null) {

elements[elementsPos++] = new Element(dex, null);

}

} catch (IOException suppressed) {

System.logE("Unable to load dex file: " + file, suppressed);

suppressedExceptions.add(suppressed);

}

} else {

DexFile dex = null;

try {

dex = loadDexFile(file, optimizedDirectory, loader, elements);

} catch (IOException suppressed) {

suppressedExceptions.add(suppressed);

}

// 其他情况,根据loadDexFile返回值确定如何创建

if (dex == null) {

elements[elementsPos++] = new Element(file);

} else {

elements[elementsPos++] = new Element(dex, file);

}

}

} else {

System.logW("ClassLoader referenced unknown path: " + file);

}

}

if (elementsPos != elements.length) {

elements = Arrays.copyOf(elements, elementsPos);

}

return elements;

}

private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,

Element[] elements)

throws IOException {

// 根据是否传入优化的目录来确定DexFile调用哪种构造方法

if (optimizedDirectory == null) {

return new DexFile(file, loader, elements);

} else {

String optimizedPath = optimizedPathFor(file, optimizedDirectory);

return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);

}

}

......

// 这里才是重点

public Class> findClass(String name, List suppressed) {

// 遍历dexElements成员变量,通过Element的findClass方法去查找需要的Class

// 找到后,直接返回!!

// 这里是热修复的关键

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;

}

......

}

上面代码不少,其实真正有用的我觉得就是findClass方法,DexPathList的findClass方法通过遍历成员变量Element[] dexElements来根据名称查找所需的Class,并将找到的Class返回(如果存在的话),这里非常非常重要!!。

写到这里,我想懂的人肯定都懂了,我们需要做的就是将没有问题的代码Dex文件插入到DexPathList的成员变量dexElements前面,这样在读取Class时首先查找的是我们没有问题的Dex文件,当查找成功后直接返回,不会进入后面的循环,从而完成问题代码的“修复”。

实现

原理都讲清楚了,剩下的就是实现了。实现代码更加简单,反射修改属性即可。下面请开始我的表演:

8527070bfe92

public class BugFixUtils {

private static final String DEX = ".dex";

// 这个8.1的源码已经无效了

private static final String OPTIMIZED_DEX_DIR = "newDex";

public static void doFix(Context context, String newDexPath) {

File dexFileDir = new File(newDexPath);

PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

if (dexFileDir.exists()) {

File[] dexFiles = dexFileDir.listFiles();

if (dexFiles != null) {

for (File dexFile : dexFiles) {

if (dexFile.getName().endsWith(DEX)) {

// 创建对象

File optimizedDirectory = new File(context.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZED_DEX_DIR);

if (!optimizedDirectory.exists()) {

optimizedDirectory.mkdirs();

}

try {

BaseDexClassLoader baseDexClassLoader = new BaseDexClassLoader(

dexFile.getAbsolutePath(),

optimizedDirectory,

null,

pathClassLoader);

// 反射获得属性

Object pathListObj = getFieldObj(Class.forName("dalvik.system.BaseDexClassLoader"), baseDexClassLoader, "pathList");

Object dexElementsObj = getFieldObj(Class.forName("dalvik.system.DexPathList"), pathListObj, "dexElements");

// 获得现在App dex文件属性

Object pathListBugObj = getFieldObj(Class.forName("dalvik.system.BaseDexClassLoader"), pathClassLoader, "pathList");

Object dexElementsBugObj = getFieldObj(Class.forName("dalvik.system.DexPathList"), pathListBugObj, "dexElements");

// 合并,顺序:新的 有Bug的

Object newElements = combineArray(dexElementsObj, dexElementsBugObj);

// 重新赋值

setFieldObj(Class.forName("dalvik.system.DexPathList"), pathListBugObj, newElements, "dexElements");

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

}

}

}

}

}

private static void setFieldObj(Class clzz, Object obj, Object value, String field) {

try {

Field declaredField = clzz.getDeclaredField(field);

declaredField.setAccessible(true);

declaredField.set(obj, value);

} catch (NoSuchFieldException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

}

}

private static Object getFieldObj(Class clzz, Object obj, String field) {

try {

Field localField = clzz.getDeclaredField(field);

localField.setAccessible(true);

return localField.get(obj);

} catch (NoSuchFieldException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

}

return null;

}

private static Object combineArray(Object newDex, Object bugDex) {

// 获得数组对象的类型

Class componentType = newDex.getClass().getComponentType();

// 获得长度

int i = Array.getLength(newDex);

int j = Array.getLength(bugDex);

// 创建新的数组

Object result = Array.newInstance(componentType, i + j);

// 把新的dex文件放在前面,有bug的放在后面

System.arraycopy(newDex, 0, result, 0, i);

System.arraycopy(bugDex, 0, result, i, j);

return result;

}

}

代码已经完成:

获取新的dex文件的位置,并根据其后缀(.dex)来判断文件是否为所需。

遍历这些文件,建立BaseDexClassLoader对象。

通过反射获得BaseDexClassLoader对象的DexPathList pathList成员变量以及pathList中的Element[] dexElements成员变量。

通过反射获得PathClassLoader对象的DexPathList pathList成员变量以及pathList中的Element[] dexElements成员变量。

将两个dexElements数组合并,注意新的dexElements数组要放在有bug的dexElements数组前面。

将合并后的数组赋值给PathClassLoader对象中的DexPathList pathList成员变量中的Element[] dexElements变量,大功告成!

测试

测试前代码:

8527070bfe92

测试前

测试前的代码只是在打开Activity的时候显示Toast“测试”,在未加载新的dex文件时正常:

8527070bfe92

测试前结果

修改后的测试代码,这里将Toast文字改编为“测试之后”,并将.class文件打包成dex文件放到sd卡的根目录下:

8527070bfe92

测试之后

8527070bfe92

dex文件

这里需要注意下,需要将App完全杀死后重新打开App,结果如下:

8527070bfe92

测试之后结果

以上源码是Android 26但是测试机是Android 5.1.1,测试可以成功。用Android 模拟器一直不成功,不知道为什么。。

总结

前面也说过,这篇文章的由来,在看源码的过程中有一种恍然大悟的感觉。之前一直听说简单热修复的原理就是把新的dex插入到旧的dex前面,但是真正让我去说个所以然,感觉真的难。不过看完源码后,原理真的很简单,真的是码读百遍,其义自见!!

8527070bfe92

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/865809
推荐阅读
相关标签
  

闽ICP备14008679号