前置
Dalvik虚拟机(DVM)
1 android系统可以进行简单的进程隔离和线程管理 每一个android进程都会在底层对应一个独立的DVM实例,其代码在虚拟机的解释下得以运行
2JVM运行的是java字节码,而DVM则运行的是专有的文件格式
dex文件在Java SE中java类会被编译为一个或者多个字节码文件(.class文件) 然后打包到jar文件 然后虚拟机会从相应的class文件和jar文件获取相应的字节码
android虽然也是用的java编程,但是在编译成class文件后 还会通过工具将class文件转换成dex文件 而后DVM通过相应文件读取指令和数据
3DVM有以下几个特征
a 专有的dex文件 每个应用中会有很多类 编译完成后会有很多的class文件 class文件中会有大量的冗余信息 而dex文件会把所有的class文件整合到一个文件中 这样做缩小的尺寸 也提高了类的查找速度 增加了对新的操作码的支持
b dex的优化 文件结构尽量简洁 使用等长的指令 借以提高解析速度 尽量扩大只读结构的大小 借以提高跨进程的数据共享
c 基于寄存器 虽然相对于基于栈的虚拟机对于硬件通用性差 但是他在代码的执行效率上更胜一筹
d 一个应用,一个虚拟机实例,一个进程 。每一个应用都对应一个DVM实例 而每一个DVM都对应一个独立进程空间。虚拟机的线程机制,内存分配管理等都更依赖底层操作系统来实现。所有Android应用的线程都对应一个Linux线程,虚拟机因而可以更多地依赖操作系统的线程调度和管理机制。
正文
一 类加载器
安卓中使用dexclassloader pathclassloader
PathClassLoader加载安卓中的dex文件
DexClassLoader 可以加载任意目录的jar/zip/dex/apk文件,但是要指定optimizedDirectory
两个类都继承BaseDexClassLoader
BaseDexClassLoader的构造方法:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
构造方法中初始化了pathList, 传入三个参数 , 分别为
dexPath 目标文件路径 。热修复时用来指定新的dex
optimizedDirectory:dex文件的输出目录
libraryPath:加载程序文件时需要用到的库路径。
parent:父加载器
二 加载类的过程
在BaseDexClassLoader中 , 紧接着构造函数的是一个叫findClass的方法 , 这个方法用来加载dex文件中对应的class文件.
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//从pathList中找到相应类名的class文件
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;
}
, 拿到初始化完成的 pathList 之后 , 根据类名找出相应的class字节码文件, 如果没有异常直接返回class.
三DexPathList
1 构造函数
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
suppressedExceptions);
}
首先 , 将传入的classLoader保存起来 , 接下来使用makePathElements方法 ,来初始化Element数组 .
那接下来无疑是分析makeDexElements()方法了,因为这部分代码比较长,引用一下大神的分析:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.创建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍历所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.将Element集合转成Element数组返回
return elements.toArray(new Element[elements.size()]);
}
总体来说,DexPathList的构造函数是将一个个的目标(可能是dex、apk、jar、zip , 这些类型在一开始时就定义好了)封装成一个个Element对象,最后添加到Element集合中。。其实,Android的类加载器(不管是PathClassLoader,还是DexClassLoader),它们最后只认dex文件,而loadDexFile()是加载dex文件的核心方法,可以从jar、apk、zip中提取出dex,但这里先不分析了,因为第1个目标已经完成,等到后面再来分析吧。
2 findclass方法
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;
}
}
在DexPathList的构造函数中已经初始化了dexElements,所以这个方法就很好理解了,只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
为什么是调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件!!!这可以从Element这个类的源码和dex文件的内部结构看出。
三热修复的实现方法
加载class会使用BaseDexClassLoader,在加载时,会遍历文件下的element,并从element中获取dex文件
方案 ,class文件在dex里面 , 找到dex的方法是遍历数组 , 那么热修复的原理, 就是将改好bug的dex文件放进集合的头部, 这样遍历时会首先遍历修复好的dex并找到修复好的类 . 这样 , 我们就能在没有发布新版本的情况下 , 修改现有的bug。虽然我们无法改变现有的dex文件,但是遍历的顺序是从前往后的,在旧dex中的目标class是没有机会上场的。
资源热修复
1反射拿到ActivityThread持有的LoadedApk容器
2遍历容器 反射替换mResDir
属性为物理补丁路径
3创建新的AssetManager 并根据补丁路径反射调用addAssetPath
将补丁加载到新的AssetManager 中
4反射获得ResourcesManager持有的Resources容器对象.
5遍历出容器中的Resources对象, 替换对象的属性为新的AssetManager, 并且根据原属性重新更新Resources对象的配置.
SO热修复
SO文件加载的时机和Dex跟资源的加载有些不一样,像Dex和资源的加载都是系统在特定的时机自动去加载,而SO加载的时机则是让开发者自己控制.开发者可以通过System类对外暴露出来的两个静态方法load和loadLibarary加载SO.这两个方法都拿到ClassLoader再通过Runtime实现的.
Sytem.loadLibrary 方法是加载app安装过之后自动从apk包中释放到/data/data/packagename/lib下对应的SO文件.
System.load 方法可以根据开发者指定的路径加载SO文件,例如/data/data/packagename/tinker/patch-xxx/lib/libtest.so.