安卓的热修复

前置
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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值