前面Java类加载器的介绍中写过关于ClassLoader的基础知识,包括了双亲委派机制、自定义ClassLoader等内容。但是,前面讲到的都是基于JVM的内容,在这里需要清楚下:Android采用的Dalvik虚拟机(DVM)和ART虚拟机(4.4版本发布)。
简单描述Android采用的虚拟机和JVM的区别
送分题(敲黑板)!!
根据广大网友描述,区别如下:
- Dalvik基于寄存器,而JVM基于栈。基于寄存器的虚拟机对于编译后变大的程序来说,在它们执行的时候,花费的时间更短。
- JVM运行java字节码,DVM运行的是其专有的文件格式Dex。
- ART与Dalvik最大的不同在于,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。
- ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这就是“时间换空间大法”。
- 预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。
如果非要深究为什么上面的一定是对的?我只能说——我也不懂。。作为菜鸡,只能站在巨人的肩膀看世界了(虽然有的的确不靠谱)。
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文件和其目录拷贝到桌面(主要是为了方便),并执行下面的命令:
这里面最后的输入路径需要注意下,输入路径需要是.class包名的上一级目录,否则生成Dex文件会报错。执行命令后会生成文件:
接着,我们将dex文件放到
/mnt/sdcard/
目录下:通过命令
adb shell dalvikvm -cp [dexFilePath] [className]
执行:OK,DVM执行Dex文件的结果已经出来了。
Android的类加载器
上面已经说了DVM可以执行Dex文件,其实我们也可以知道不管采用什么虚拟机,还是需要将执行的代码(字节码)加载到内存,最终执行。我们先看下Android里的ClassLoader:
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<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 调用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<File> nativeLibraryDirectories;
private final List<File> systemNativeLibraryDirectories;
private IOException[] dexElementsSuppressedExceptions;
......
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
......
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
// 根据传入的dex的路径生成Element数组
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
......
}
......
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> 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<Throwable> 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文件,当查找成功后直接返回,不会进入后面的循环,从而完成问题代码的“修复”。
实现
原理都讲清楚了,剩下的就是实现了。实现代码更加简单,反射修改属性即可。下面请开始我的表演:
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
变量,大功告成!
测试
测试前代码:
测试前的代码只是在打开Activity的时候显示Toast“测试”,在未加载新的dex文件时正常:
修改后的测试代码,这里将Toast文字改编为“测试之后”,并将.class文件打包成dex文件放到sd卡的根目录下:
这里需要注意下,需要将App完全杀死后重新打开App,结果如下:
以上源码是Android 26但是测试机是Android 5.1.1,测试可以成功。用Android 模拟器一直不成功,不知道为什么。。
总结
前面也说过,这篇文章的由来,在看源码的过程中有一种恍然大悟的感觉。之前一直听说简单热修复的原理就是把新的dex插入到旧的dex前面,但是真正让我去说个所以然,感觉真的难。不过看完源码后,原理真的很简单,真的是码读百遍,其义自见!!