前面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文件

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

放到目录

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

执行结果

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

Android的类加载器

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

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文件,当查找成功后直接返回,不会进入后面的循环,从而完成问题代码的“修复”。

实现

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

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卡的根目录下:

测试之后

dex文件

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

测试之后结果

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

总结

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

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

  1. Android热修复Java类_Android 热修复(一)

    名词: dex:java文件编译class 然后生成 dex文件在Android上运行: 1.dex分包: 2.找出出现问题的dex文件进行替换操作 3.下载dex文件,静默替换有问题的dex文件,进 ...

  2. Android Gradle 构建工具(Android Gradle Build Tools)是什么?

    转载地址:http://mrfu.me/android/2015/07/17/New_Android_Gradle_Build_Tools/ 译者地址:[翻]一览新的 Android Gradle 构 ...

  3. dll修复工具下载,dll修复工具注意事项

    Dll文件的缺失相信很多人都遇见过吧,只要缺失了一个这样的dll文件,我们的游戏或者软件程序就启动不了了,所以我们就需要去修复它,目前修复有几种方法,最简单的,最适合电脑小白的,那就是dll修复工具了 ...

  4. MYSQL安装时缺少MSVCP120.dll和MSVCR140.dll的修复工具,本人已修复并成功安装使用MYSQL

    MYSQL安装时缺少MSVCP120.dll和MSVCR140.dll的修复工具,本人已修复并成功安装使用MYSQL,值得尝试此方案 文件下载地址:MYSQL安装时缺少MSVCP120.dll和MSV ...

  5. Android热修复——深入剖析AndFix热修复及自己动手实现

    前言 去年写过一篇热修复的文章,那时候刚开始接触,照猫画虎画的还算比较成功.但是那种修复需要重新启动APP,也就是在JAVA层实现的热修复.我们知道目前Android主流的修复还有在Native层实现 ...

  6. Android 框架学习5:微信热修复框架 Tinker 从使用到 patch 加载、生成、合成原理分析

    这篇文章是基于内部分享的逐字稿内容整理的,现在比较喜欢写逐字稿,方便整理成文章. 文章目录 目录 Tinker 介绍 使用 TinkerApplicaition ``SampleApplicaitio ...

  7. Android每周一轮子:Nvwa(热修复)

    前言 (废话) 最近发现了一个问题,一些平时博客写的很多的程序员,反倒在日常的工作中,却是业务写的很一般,只会摆理论的人,甚至还跑出来教别人如何找工作,如何做架构,其实自己都没搞明白.但是受众的分层导 ...

  8. Android 性能优化工具 TraceView 简单使用

    背景 最近产品以及测试大佬反应快搜桌面进入搜索页面跳转较为缓慢,影响体验,为了优化这个问题,特地学习Android 性能优化工具 TraceView的 简单使用,这才有了本文. 正文 如下图打开and ...

  9. android app性能测试工具,Android 性能测试 - 内存

    1.内存了解 在Android App的性能优化的各个部分里,内存方面的知识较多且不易理解,内存的问题绝对是最令人头疼的一部分,需要对内存基础知识.内存分配.内存管理机制等非常熟悉,才能排查题. 1. ...

  10. android 开发小工具,Android 开发者必备的八款小工具

    在做Android 开发过程中,会遇到一些小的问题,虽然自己动手也能解决,但是有了一些小工具,解决这些问题就得心应手了,今天就为大家推荐一些Android 开发必备的小工具. Android Pixe ...

最新文章

  1. 使用C#格式化字符串
  2. 关于前后端分离我的理解
  3. 一些.net持久化框架的例子
  4. 教育机构如何提升在线教育技术能力? | 云+社区技术沙龙
  5. java的修饰符_java默认的修饰符是什么
  6. oracle8i substr,Oracle中的INSTR,NVL和SUBSTR函数的用法详解
  7. mysql queries 很大,mysql优化通常使用的几种方法
  8. 凤凰系统中禁用触摸屏
  9. 适合公司年会的4个热门互动小游戏
  10. 网页进行pdf打印_将多个pdf文档合并为一个pdf
  11. java小项目之简单聊天室
  12. 快来直播:坦然面对东方甄选与股价跌宕起伏
  13. c# 时间格式化为英文_C# DateTime日期格式化
  14. 刘东明微信营销二十五式初探(一)
  15. 希尔排序|Golang
  16. 【ROS入门学习05|自定义话题消息,并且编程实现publisher和subscriber】
  17. 广西大学计算机仿真实验,基于Simulink的汽车动力性仿真实验设计
  18. 基于JAVA消防安全应急培训管理平台计算机毕业设计源码+数据库+lw文档+系统+部署
  19. 3D标签云/滚动词条
  20. $merge()合并两个数组方法

热门文章

  1. ARMv7的OP-TEE源代码的获取和编译
  2. 场编码MBAFF相关
  3. N的阶乘的长度 V2(斯特林近似)
  4. Floyd算法(附例题)
  5. 面向对象7:项目二的总结
  6. tensorflow模型部署与python java API线上调用
  7. C++数据范围及字节对照表
  8. 有意思的签到题集合~~
  9. c语言程序设计黄迎久,C语言程序设计教程_黄迎久、庞润芳主编 赵军富、徐扬、胡晓燕、贾茹副主编_9787302418528_...
  10. Java vbnullstring_VB中Null、Empty、Nothing及vbNullString的区别