JAVA进阶(1)—— 类加载器
类加载器
一、前言
1、动态加载
1)插件化 —— 当我们项目越来越大,我们可以通过插件化来减少应用的内存,然后动态加载那些插件。
2)热修复 —— 如果我们的应用频繁的更新,频繁的发布新版本,肯定会造成用户体验下降 ,那么可以用动态加载技术在不发布新版本的情况下更新一些模块。
那么既然要用动态加载,就肯定涉及到类加载器。
2、JVM使用Java类
Java源程序(.java 文件)在经过Java 编译器编译之后就被转换成Java字节码(.class 文件)。类加载器负责读取Java字节码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个Java类。通过此实例的newInstance()方法就可以创建出该类的一个对象
二、Java中类加载器
1、类加载器与类本身确定类的唯一性
对于一个类,这个类本身和真正加载它的类加载器共同确定其在虚拟机中的唯一性。 使用两个类加载器进行加载同一个类,那么这两个类是不相等的,那么虚拟机中会存在两个同名的类。同一类加载器实例,同名的类仅加载一次,下次通过取缓存获取。
2、类加载器
2.1、作用
根据一个指定类,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例
2.2、ClassLoader方法
-
getParent()
返回该类加载器的父类加载器 -
loadClass(String name)
启动类加载 -
defineClass(String name, byte[] b, int off, int len)
从二进制流中加载Class,final修饰 -
getSystemClassLoader()
获取系统类加载器,static修饰 -
findClass(String name) Class<?>
其中调用了defineClass方法 -
findLoadedClass(String name) Class<?>
返回已被虚拟机加载的类
2.3、类加载器的树状结构
类加载器的树状结构说明:对应的是类的双亲委派机制的逻辑关系
2.4、Java三种预定义类型类加载器
启动类加载器(Bootstrap ClassLoader,也称为引导类加载器)
- 加载JAVA核心库,并且是虚拟机识别的(这点很重要)类加载到虚拟机中。
- 用本地代码实现的类加载器,不继承
java.lang.ClassLoader
类 - 无法直接获取引用并使用
扩展类加载器(Extension ClassLoader)
- 加载JAVA的扩展库
- getParnet()返回null
- 可以获取并使用
应用程序类加载器(Application ClassLoader,也称为系统类加载器)
- 根据应用的类路径(CLASSPATH)来加载类
- ClassLoader中的getSystemClassLoader()方法的返回值
- 可以直接使用这个类加载器。如果没有自定义类加载器,一般情况下该加载器是程序中的默认的类加载器
2.5、类加载双亲委派机制
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
实现双亲委派模型的代码在loadClass()
方法中:
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//检查请求加载的类是否已经加载过了
Class<?> c = findLoadedClass(className);
if (clazz == null) {
try {
//尝试使用父类加载器加载,父类加载器不为空,则使用父类加载器尝试加载
if(parent != null){
c = parent.loadClass(className, false);
}
//如果父类加载器为null,则使用启动类加载器作为父加载器
else{
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出异常,说明父类加载器无法完成加载请求
}
if (c == null) {
//父类无法完成加载时调用本身的findClass方法来进行类加载
c = findClass(className); //其中调用了defineClass()方法
}
}
if(resolve){
resolveClass(c);
}
//如果加载过了,就直接返回已经加载的类
return clazz;
}
初始化加载器:启动类的加载过程,通过调用loadClass来实现
定义加载器:真正完成类的加载工作,通过调用defineClass来实现
优点
大家都知道Object类是个基础类,如果我们自己写了一个Object类,那么如果没有双亲委派模型的话,再加上我们没有用启动类加载器去加载我们写的这个Object类的话,系统中会存在两个Object类(参考上述的类在虚拟机中的唯一性)。
有了双亲委派模型,我们写了一个Object类,会先去检查它是否加载了(肯定已经加载了),那么我们写的这个就不会
被重复加载,也就保证了基础类的唯一性。就算没有检查,根据上面关于启动类加载器的介绍,必须是
虚拟机识别的,Object存放在rt.jar中,我们写的不会被识别。
基础类在任何环境下都是同一个类(即加载器在任何情况下都是同一个),这就是
双亲委派模型的作用。每次加载请求都会委派给处于最顶端的启动类加载器进行加载,虚拟机识别rt.jar,那么就
保证了每次都是由启动类加载器加载Object。
2.6、自定义类加载器
场景1:应用通过网络传输的加密字节码,此时需要先解密再定义类
场景2:加载存储在文件系统上的 Java 字节代码
自定义类加载器符合双亲委派模型
我们根据上面的介绍知道,双亲委派模型的逻辑都在loadClass()方法中,那么我们为了不破坏双亲委派模型,自定义类加载
器时不去重写loadClass()方法,而是重写findClass()方法,将自己的类加载逻辑写到findClass()方法中,在loadClass()方法中,最后父类加载器无法加载的时候,调用的就是findClass()方法。这样我们就保证了我们自定义的类加载器是符合双亲委派模型的。如果重写loadClass()方法,会出现一系列错误,比如基础类加载不上等。
父类加载器是加载此类加载器 Java 类的类加载器(一般为系统类加载器)
二、Android类加载器
1、基本介绍
android中的虚拟机是Dalvik,它不是标准的Java虚拟机,所以在类加载机制上,和Java中的类加载器有一些区别。
在java标准的虚拟机中,如果自定义类加载器,会继承ClassLoader,并重写findClass()方法,在内部调用defineClass()
去从一个二进制流中加载Class。
在Android中,defineClass()方法什么都没做。那么在Dalvik虚拟中,动态加载类就需要另外由ClassLoader派生出的两个类:DexClassLoader和PathClassLoader。这两个类重载了ClassLoader的findClass()
方法,并没有重写loadClass()方法,所以这两个类加载器符合双亲委派模型。
注意:Dalvik虚拟机识别的是dex文件,而不是class文件,因此,加载的是dex文件、apk文件(包含dex文件)或jar文件(dx命令执行过后的jar,首先将.jar编译成.dex文件,然后再压缩成.jar)。
2、相关类方法
PackageManager
-
queryIntentActivities(Intent intent, int flags) List<ResolveInfo>
返回匹配该intent的Activity信息。
ResolveInfo
activityInfo ActivityInfo
ActivityInfo
applicationInfo ApplicationInfo
ApplicationInfo
-
sourceDir
应用的apk的安装目录,重要! -
nativeLibraryDir
应用的jni库的目录
3、PathClassLoader
-
PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent)
dexPath:指定要加载的dex文件路径;librarySearchPath:c/c++依赖的本地库路径,可以为null;parent:上一级的类加载器,一般为this.getClassLoader() -
loadClass(String name) Class<?>
继承自ClassLoader的方法
4、DexClassLoader
-
DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent)
dexPath:dex文件路径;optimizedDirectory:dex文件解压缩后存放目录;librarySearchPath:c/c++依赖的本地库路径,可以为null;parent:上一级的类加载器,一般为this.getClassLoader()
5、对比
PathClassLoader:不能主动从zip包中释放出dex,只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在手机的data/dalvik-cache中存在缓存的dex文件)。
DexClassLoader:支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。
6、动态加载的类使用方式
- 反射调用,但过多的反射有一定的性能开销,代码复杂凌乱。
- 使用接口编程的方式来调用对应的方法,毕竟.dex文件也是我们自己维护的,所以可以把方法抽象成公共接口,把这些接口也复制到主项目里面去,就可以通过这些接口调用动态加载得到的实例的方法。
7、热修复原理
原理:BaseDexClassLoader调用findClass去加载类的时候,会调用DexPathList#findClass()
如下图:
因此把修复后的dex插入到最前面,遍历开始找到class就直接返回,那么有bug的dex或class就不会被加载。
public class HotFixEngine {
public static final String DEX_OPT_DIR = "optimize_dex";//dex的优化路径
public static final String DEX_FILE_E = "dex";//扩展名
public static final String FIX_DEX_PATH = "fix_dex";//fixDex存储的路径
/**
* 入口方法,给外部调用<br/>
* 复制SD卡中的补丁文件到dex目录
*/
public static void copyDexFileToAppAndFix(Context context, String dexFileName) {
File path = new File(Environment.getExternalStorageDirectory(), dexFileName);
if (!path.exists()) {
Toast.makeText(context, "没有找到补丁文件", Toast.LENGTH_SHORT).show();
return;
}
if (!path.getAbsolutePath().endsWith(DEX_FILE_E)) {
Toast.makeText(context, "补丁文件格式不正确", Toast.LENGTH_SHORT).show();
return;
}
File dexFilePath = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
File dexFile = new File(dexFilePath, dexFileName);
if (dexFile.exists()) {
dexFile.delete();
}
//copy
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(path);
os = new FileOutputStream(dexFile);
int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
if (dexFile.exists()) {
//复制成功,进行修复,重要!!!!
new HotFixEngine().loadDex(context, dexFile);
}
path.delete();//删除sdcard中的补丁文件,或者你可以直接下载到app的路径中
is.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* fix
*
* @param context
*/
public void loadDex(Context context, File dexFile) {
if (context == null) {
return;
}
File fixDir = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
//mrege and fix
mergeDex(context, fixDir, dexFile);
}
/**
* 合并dex
*
* @param context
* @param fixDexPath
*/
public void mergeDex(Context context, File fixDexPath, File dexFile) {
try {
//创建dex的optimize路径
File optimizeDir = new File(fixDexPath.getAbsolutePath(), DEX_OPT_DIR);
if (!optimizeDir.exists()) {
optimizeDir.mkdir();
}
//加载自身Apk的dex,通过PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//找到dex并通过DexClassLoader去加载
//dex文件路径,优化输出路径,null,父加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), optimizeDir.getAbsolutePath(), null, pathClassLoader);
//获取app自身的BaseDexClassLoader中的pathList字段
Object appDexPathList = getDexPathListField(pathClassLoader);
//获取补丁的BaseDexClassLoader中的pathList字段
Object fixDexPathList = getDexPathListField(dexClassLoader);
Object appDexElements = getDexElements(appDexPathList);
Object fixDexElements = getDexElements(fixDexPathList);
//合并两个elements的数据,将修复的dex插入到数组最前面
Object finalElements = combineArray(fixDexElements, appDexElements);
//给app 中的dex pathList 中的dexElements 重新赋值
setFiledValue(appDexPathList, appDexPathList.getClass(), "dexElements", finalElements);
Toast.makeText(context, "修复成功!", Toast.LENGTH_SHORT).show();
//最后需要通过Android build-tools 中的dx命令打包一个没有bug的dex
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获得pathList中的dexElements
*
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
/**
* 获取指定classloader中的pathList字段的值(DexPathList类型)
* BaseDexClassLoader是PathClassLoader和DexClassLoader的父类
*/
public Object getDexPathListField(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 获取一个字段的值
*
* @return
*/
public Object getField(Object obj, Class<?> clz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = clz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
/**
* 为指定对象中的字段重新赋值
*
* @param obj
* @param claz
* @param filed
* @param value
*/
public void setFiledValue(Object obj, Class<?> claz, String filed, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = claz.getDeclaredField(filed);
field.setAccessible(true);
field.set(obj, value);
}
/**
* 两个数组合并
*
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}
参考文献
Android中的动态加载机制
Android动态加载学习总结(一):类加载器
Android 热修复(全网最简单的热修复讲解)