Android类加载机制

Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。
只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。

首先看一下Android平台中几个常用的加载相关的类,以及他们的继承关系。

  • PathClassLoader是用来加载Android系统类和应用的类。
  • DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。

ClassLoader类的主要职责:根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。 (还可以加载图片等,这里不讨论)

ClassLoader中的主要方法及含义

下面这段代码时loadClass的实现,可以看出首先会查找加载类是否已经被加载了,如果是直接返回。否则,通过findClass()查找

 protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
// this is the defining class loader; record the stats
}
return c;
}

BaseClassLoader中的findClass函数:实际是在一个pathList中去查找这个类。

 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;
}

DexPathList的源码可知,成员变量dexElements用来保存dex数组,而每个dex文件其实就是DexFile对象。遍历dexElements,然后通过DexFile去加载class文件,加载成功就返回,否则返回null,看到这里应该基本知道我们想干啥了,我打算在dexElements上面做手脚,可以通过反射来加载。

 /*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk"; /** class definition context */
private final ClassLoader definingContext; /**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements; /**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile; if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}

实际程序运行时我们只需要将本地加载的dex文件插入到DexPathList中的dexElements中,后续程序运行时就会自动到该变量中去查找新的类,这就是该篇讲的热修复的原理。

补丁包dex文件生成

如果某个APP远程出现bug,那么开发者如何生成一个新的dex文件,然后通过网络下发到客户端呢?

1.到该android sdk的该目录下:(不一定是23.0.1也可以是其它版本号)

2. 将要生成的dex的class文件(全路径)放到该目录下(对应上图中的com文件夹)。class文件可以从Android Studio的该目录拷贝:

3. 运行如下命令即可根据MyTestClass.class文件本地生成补丁包: patch.dex

.\dx.bat --dex --output=patch.dex .\com\xxx\xxx\hotfix_qqzone\MyTestClass.class

通过反射将本地dex注入

前面已经介绍了ClassLoader的一些接口和DexPathList。下面将介绍如何一步一步将本地(可能是通过网络从服务端获取的)dex文件动态加载到内存中。

1. 通过反射获取dexElements

 private static Object getDexElementByClassLoader(ClassLoader classLoader) throws Exception {
Class<?> classLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = classLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader); Class<?> pathListClass = pathList.getClass();
Field dexElementsField = pathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElements = dexElementsField.get(pathList); return dexElements;
}

2. 获取本地dex补丁包,生成dexElement,并合并到已有的dexElements中(通过combineArray函数)

 public static void loadFixedDex(Context context,String path){

 		String optimizeDir = context.getDir("odex",Context.MODE_PRIVATE)+File.separator+"opt_dex";
File fopt = new File(optimizeDir);
if(!fopt.exists()){
fopt.mkdirs();
}
//1.加载应用程序的dex
try {
PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
//2.加载指定的修复的dex文件。
DexClassLoader classLoader = new DexClassLoader(
path,//String dexPath,
fopt.getAbsolutePath(),//String optimizedDirectory,
null,//String libraryPath,
pathLoader//ClassLoader parent
);
//3.合并
Object path_pathList = getPathList(pathLoader);
Object dex_pathList = getPathList(classLoader);
Object path_DexElements = getDexElements(path_pathList);
Object dex_DexElements = getDexElements(dex_pathList);
//合并完成
Object dexElements = combineArray(dex_DexElements,path_DexElements);
//重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements); } catch (Exception e) {
e.printStackTrace();
}
}
private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj,value);
}

问题1:

假设class.dex中有一个bug.class出现了bug,现在希望获通过一个fixbug.class生成了一个补丁包patch.dex。 如果我们APP初始化的时候就加载pathc.dex 不会出现问题,下次调用bug.class会优先使用patch.dex中的bug.class而不是.

总结: 当代码中已经调用了有问题的类,而没有加载patch.dex,后续加载将不会再起作用。

问题2: CLASS_ISPREVERIFIED标记问题

如果待修复的类bug.class中没有引用到class.dex中其它类,则bug.class不会被打上该标记,热修复不会出现问题。

如果bug.class中引用了class.dex 情况。 // 后续有时间再补充。

实际业务使用场景:

项目本地有一套代码逻辑,当本地代码执行失败后,会从服务端获取补丁包(.dex文件)。因为提前知道补丁包的类名和接口,所以通过DexClassLoader 将补丁信息加载成一个类,然后再通过反射构造一个该类的对象。同时可以通过反射调用类里面相关的接口了,这样就相当于对本地的代码进行了替换也是一种修复过程。

Class<?> clazz;
DexClassLoader dexClassLoader = new DexClassLoader(patchSaveDir + patchName,
dataDir, "", getClass().getClassLoader());
clazz = dexClassLoader.loadClass(className);
Constructor constructor = clazz.getDeclaredConstructor();
Object object = constructor.newInstance();

自此,简单的类加载机制和热修复就介绍完毕了!

参考:

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

https://www.jianshu.com/p/a620e368389a

Android类加载机制及热修复实现的更多相关文章

  1. Android 类加载原理 和热修复——深入浅出原理与实现

    一.简述 热修复无疑是这2年较火的新技术,是作为安卓工程师必学的技能之一.在热修复出现之前,一个已经上线的app中如果出现了bug,即使是一个非常小的bug,不及时更新的话有可能存在风险,若要及时更新 ...

  2. Android 插件化和热修复知识梳理

    概述 在Android开发中,插件化和热修复的话题越来越多的被大家提及,同时随着技术的迭代,各种框架的发展更新,插件化和热修复的框架似乎已经日趋成熟,许多开发者也把这两项技术运用到实际开发协作和正式的 ...

  3. Android插件化与热修复(六)-微信Tinker原理分析

    Tinker热修复原理分析 热补丁技术是在用户不需要重新安装应用的情况下实现应用更新,可快速解决一些线上问题.热补丁省去了Android应用发布版本的成本,而且用户端的更新也是无感知的. Tinker ...

  4. 《android基于andFix的热修复方案》思路篇

    1:需求背景 项目上线之后,发现BUG需要修复(比如安卓兼容性等测试难以发现的问题),频繁的更新影响用户体验 2:方案要求 静默下载,耗费流量少,打完补丁后立刻生效,不用重启apk 3:解决思路 3. ...

  5. Android Gradle项目Hotfix热修复技术的接入

    https://github.com/AItsuki/HotFix Issues MAC系统无法自动打包补丁,原因可能是路径分隔符问题 使用谷歌multidex分包后无法注入代码(开启multidex ...

  6. 《android基于andFix的热修复方案》实战篇

    有篇文章说的比较简洁,大家可以参考下:AndFix使用说明 下面说说实际使用中遇到的问题 1:如何继承到gradle项目中 dependencies { compile 'com.alipay.eul ...

  7. Android 热补丁和热修复

    参考: 各大热补丁方案分析和比较 Android App 线上热修复方案 1. Xposed Github地址:https://github.com/rovo89/Xposed 项目描述:Xposed ...

  8. android Qzone的App热补丁热修复技术

    转自:https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731 ...

  9. Android热修复原理(一)热修复框架对比和代码修复

    在Android应用开发中,热修复技术被越来越多的开发者所使用,也出现了很多热修复框架,比如:AndFix.Tinker.Dexposed和Nuwa等等.如果只是会这些热修复框架的使用那意义并不大,我 ...

随机推荐

  1. Oracle定时任务小案例

    需求简述 一个数据表中包含此数据的录入时间,此数据的初始状态是有效,五天后系统自动置该数据的状态为无效. 方案 写一个存储过程,用于更新字段(改状态): 写一个job,用于定时执行存储过程: 方案逻辑 ...

  2. 推荐一款有趣的APP-种子习惯

    在女朋友的影响下接触到了种子习惯的app,一开始不觉得它有多大魅力,只知道它有契约的功能,有很多人分享自己的习惯,记录生活中的点滴. 毕业回家的寒假,家里比较冷,每天起床都需要莫大的勇气,挣扎半天最终 ...

  3. linux下 mysql5.7.20安装(精华)

    在linux 系统中mysql配置文件的读取顺序为: /etc/my.cnf /etc/mysql/my.cnf /usr/local/mysql/etc/my.cnf ~/.my.cnf 第一步 创 ...

  4. 完整的站内搜索实战应用(Lucene.Net+盘古分词)

    首先自问自答几个问题,以让各位看官了解写此文的目的 什么是站内搜索?与一般搜索的区别? 多网站都有搜索功能,很多都是用SQL语句的Like实现的,但是Like无法做到模糊匹配(例如我搜索". ...

  5. 洛谷 P2401 不等数列

    其实有两种方法来解这道题# 第一种:找规律(非正经) 一看,这玩意像是个杨辉三角,还左右对称呢 因为新插入一个数$n$,有$n+1$个位置可以选,所以总数就乘$n+1$,对应的$f[n+1][i]$也 ...

  6. Java集合中的LinkedHashMap类

    jdk1.8.0_144 本文阅读最好先了解HashMap底层,可前往<Java集合中的HashMap类>. LinkedHashMap由于它的插入有序特性,也是一种比较常用的Map集合. ...

  7. 分布式日志收集系统:Flume

    Flume知识点: Event 是一行一行的数据 1.flume是分布式的日志收集系统,把收集来的数据传送到目的地去. 2.flume里面有个核心概念,叫做agent.agent是一个java进程,运 ...

  8. Python解析Yahoo的XML格式的天气预报数据

    以下是Yahoo天气预报接口xml格式数据: <rss xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0" xm ...

  9. Java技术总结

    1.在非空判断是一定把not null 判断写前边,否则如果为空先判断size为0会报错 String str = null; if(str !=null&&str.length()& ...

  10. MySQL聚集索引和非聚集索引

    索引分为聚集索引和非聚集索引,mysql中不同的存储引擎对索引的底层实现可能会不同,这里只关注mysql的默认存储引擎InnoDB. 利用下面的命令可以查看默认的存储引擎 show variables ...