了解热修复,需要有点预热的知识,先从class文件和dex文件说起
class文件和dex文件
class文件
什么是class文件
他是一种文件格式
简单说,就是能被JVM虚拟机识别、加载、并执行的文件格式
而且除了java语言,还有很多其他语言也可以编译出class文件,当然还有kotlin
如何手动编译出一个class文件
很简单
javac hello.java
class文件的作用
记录一个类文件里的所有信息,记住是一个类文件,而且是所有信息
Class类文件结构
详细的可参考【深入Java虚拟机】之二:Class类文件结构
这里简要说一下:
-
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。
-
下表列出了Class文件中各个数据项的具体含义:
magic
每个Class文件的头4个字节称为魔数(magic),它的唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。它的值固定为0xCAFEBABE。
version
紧接着magic的4个字节存储的是Class文件的次版本号和主版本号,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件。
constant_pool
常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。
常量池是由一组constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。
常量池计数器constant_pool_count 的值 =constant_pool表中的成员数+ 1。constant_pool表的索引值只有在大于 0 且小于constant_pool_count时才会被认为是有效的。
access_flag
this_class、super_class、interfaces
fields
methods
attributes
来个大图
好了,看看二进制文件究竟长什么样
这个是使用一个工具来查看class文件的内容
为什么Android没使用class文件,而是创造了dex文件呢
- class文件内存占用大,不适合移动端,最关键就是一个class文件只能表述一个类文件的所有属性
- 堆栈的加载模式,加载速度较慢
- 文件IO操作多,类查找慢
dex文件
什么是dex文件
能被DVM虚拟机识别、加载、并执行的文件格式
如何手动编译一个dex文件
在build-tools里面找到dx.bat
要使用dx命令,记得配置环境变量
- javac命令 生成class文件
javac hello.java
- dx命令 生成dex文件
dx --dex --output hello.dex hello.class
- adb命令把hello.dex文件放到手机内存卡
adb push hello.dex /storage/emulated/0
- 进入shell
adb shell
- dalvikvm命令 执行dex文件里的hello方法
注意dex文件必须在Andriod手机执行,因为手机里才有DVM虚拟机
dalvikvm -cp /sdcard/hello.dex hello
dex文件的作用
一个class文件只是记录一个Java类的所有信息
但是一个dex记录所有类文件的信息,是整个工程的信息
dex文件结构
上图中的文件头部分,记录了dex文件的信息,所有字段大致的一个分部;
索引区部分,主要包含字符串、类型、方法原型、域、方法的索引;
索引区最终又被存储在数据区,其中链接数据区,主要存储动态链接库,so库的信息。
dex文件长什么样子呢
来张大图
dex与class异同
当java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右
编年体与纪传体
纪传体通过记叙人物活动反映历史事件的体裁,通过记叙人物活动,反映历史事件。 如:《秦始皇本记.class》《项羽本纪.class》《高祖本纪.class》
编年体是中国传统史书的一种体裁,它是以年代为线索编排有关历史事件。编年体史书以时间为中心,按年、月、日顺序记述史事。因为它以时间为经,以史事为纬,比较容易反映出同一时期各个历史事件的联系。 例如:《春秋.dex》《左传.dex》《资治通鉴.dex》。
JVM虚拟机简介
jvm整体结构与组成
内存里存储class文件的不同部分,对应内存空间里的不同部分
编译流程
类加载器
jvm的classloader与Android里的classloader区别较大
下图为jvm的类加载器,
Android的类加载器是热修复的核心,接下来会专门说
类加载流程
jvm内存管理和垃圾回收
java栈区
java栈帧
每个方法从调用到执行完成,就是对应一个栈帧在虚拟机从入栈到出栈的过程
栈帧里包含局部变量表、栈操作数、动态链接、方法入口
A方法调用B方法,就会在调用B方法代码时,java虚拟就就会创建一个保存B方法的栈帧,然后压入栈区,当B方法执行完后,这个栈帧就会弹出栈区,这就是使我们经常说的,栈内存不需要我们管理,局部变量会在方法调用结束后,自动回收。
另外,从这里可以看出,每个方法对应一个栈帧,如果递归方法嵌套太深,当栈的深度大于jvm所允许的最大深度时候,会引起Stack Overflow,栈溢出,所以递归慎用,
本地方法栈
为native方法服务的,也是通过栈帧实现对本地方法的调用
方法区
存储虚拟机加载的类信息、常量、静态变量、及时编译器编译后的数据
这块区域,永远占据内存,知道退出进程
所以常量、静态变量生命周期很长,只有App退出,才会被回收,所以,很多内存泄漏都是不合理使用静态变量引起的
堆区
所有通过new创建的对象的内存都在堆区分配
是虚拟机中最大的一块内存,是GC要回收的部分
新生代与老生代,简单说,刚刚创建的对象会存在新生代里,当新生代对象越来越多,内存不足时候,jvm会通过自己的一套算法,把对象从新生代移动到老生代,这样新生代就会多出一部分空间了,还能接受新的对象。当新生代和老生代的内存都满了,再来对象就会oom
为什么要分新生代+老生代
这是为了让开发者动态调整新生代和老生代的大小,例如在做即时通讯时,临时的消息对象创建的比较多,就可以把新生代这块区域调整大一些,便于新对象的分配
垃圾回收
引用计数算法
引用计数器:被引用+1,引用销毁-1,为0,则可以被销毁
循环引用的时候,此算法失效
可达性算法
被GCRoot直接或者间接引用的对象,就不可销毁
引用类型
强软弱虚
弱引用的创建与使用
垃圾回收算法
标记-清除算法
好处:不需要让对象进行移动,仅需要对不存活的对象进行处理,在存活对象较多时候,执行效率高效,但是内存碎片很多
复制算法
好处:当存活的对象比较少时,较为高效,但是需要另外一块空间,用于管理移动
标记-整理算法
- 先遍历把可回收对象扫描出来,如B
- 扫描清除未标记对象
- 把存活的对象,进行移动,没有内存碎片
以上三种算法各有优缺点,虚拟机根据不同情况,采用不同算法,进行垃圾回收
触发回收
- jvm无法为新对象创建内存了
- 手动调用System.gc()方法(并不会马上执行gc)
- 低优先级的gc线程,被运行时就会执行
Dalvik 虚拟机与Jvm异同之处
- 执行文件不同,一个是class文件,一个是dex
- 类加载系统区别较大
- Dalvik 可以同时存在多个,Jvm只能同时存在一个
- Dalvik是基于寄存器的,jvm是基于栈的
jvm的方法调用是就栈的,前面说的栈帧
Dalvik是基于寄存器的,寄存器是比内存更快的存储介质
ART虚拟机
虽然Dalvik虚拟机已经不错了,但是google工程师研发了ATR虚拟机,更加高效
- DVM使用JIT将字节码转换为机器码,效率低
app每次运行都会把字节码转换为机器码,再去执行,退出应用,在进入app,又会再次把字节码转为机器码,效率很低的
- ART采用的是AOT预编译技术,执行速度更快
在app安装时候,就把字节码转为本地机器码,存在本地,因此,只要app启动,直接执行机器码,而不是每次转换。
但是采用ART预编译技术,app安装时间快比较长,而且在手机里占用空间多
空间换时间
Classlodaer
java里的classloder
android的classloader
classloader种类
- BootClassLoader
加载framework层的字节码文件
- PathClassLoader
加载安装到系统里的app的class文件
- DexClassLoader
加载指定目录的class文件
- BaseDexClassloader
PathClassLoader和DexClassLoader的父类
其实一个app最少需要BootClassLoader和PathClassLoader才能正常运行
我们打印下app里的classlodaer
// 打印所有的ClassLoader
var classLoader = classLoader
if (classLoader != null) {
Log.e("cjx", "ClassLoader---$classLoader")
while (classLoader.parent != null) {
classLoader = classLoader.parent
Log.e("cjx", "ClassLoader---$classLoader")
}
}
ClassLoader的特点
双亲代理模式
- classloader加载字节码时,先询问当前classloader是不是加载过此类,如果加载过,直接返回(不会重复加载字节码)
- 如果没有加载过,询问父classloader是不是加载过,如果加载过返回parent加载过的字节码文件
- 如果整个继承线路都没加载过这个字节码,才会由子classloader完成加载
由此可见,一个字节码文件被任意一个classLoader加载过,就不会被其他classLoader加载了,提高了加载效率,也带来了另外特性
类加载共享功能
一个字节码文件一旦被顶层classLoader加载过,就会被整个继承体系所共有
类加载隔离功能
不同继承路线的classLoader加载的类,肯定不是同一个类,防止被冒充
例如String这个类,肯定在顶层的classLoader里会把它加载,这样就避免,你自己写个classLoader来篡改string这个类的加载过程
什么样的类才能叫做是同一个类呢
同一个包名+同一个类名+同一个类加载器加载的类,才叫同一个类
ClassLoader的源码
如果都找不到,会走findClass方法,看一下这个方法
那么ClassLoader有哪些子类呢
间接子类:DexClassloader
上面的第二个参数很重要,这个路径是系统内部的路径,就是因为这个参数,才能去把未安装到app里的dex文件,加载进来
间接子类:PathClassLoader
其实这两个间接地子类,什么也没做,只是一个能加载外部的dex文件,一个只能加载apk内部的文件,主要逻辑还是他们的父类BaseDexClassloader实现的,我们接着看BaseDexClassloader的findClass方法,看看是如何加载dex文件的
直接子类:BaseDexClassloader
发现直接调用的是DexpathList的findclass方法
Element是类DexPathList的一个内部类,它其中重要的一个变量就是DexFile,就是dex文件。
看看这个Element[]是怎么实现的
来到makePathElement方法
makePathElements方法核心作用就是将指定路径中的所有文件转化成DexFile同时存储到到Element[]这个数组中。nativeLibraryDirectories 就是lib库了。
最终在findclass方法中实现。
接着看看dexFile的loadClassBinaryName方法,我们进入DexFile这个类
回顾一下,我们的源码解析经历了些什么
- 首先看了ClassLoder 的双亲委托模式的实现,发现最终指向了findClass()这个方法
- 然后发现他是一个空实现,他等着子类去实现
- ClassLoader有一个直接子类BaseDexClassloader和两个间接子类DexClassloader 、PathClassLoader其实这两个间接地子类,什么也没做,只是DexClassloader能加载外部的dex文件,PathClassLoader只能加载apk内部的文件,主要逻辑还是他们的父类BaseDexClassloader实现的
- 在BaseDexClassloader里发现findClass,调用的是DexPathList的findClass方法
- 在DexPathList里,先看到一个Element这个内部类,他里面有个重要的变量叫DexFile,Element[]是通过makePathElement实现的
- makePathElement遍历所有文件,把所有dex加载为dexFile,并且存到Element[]里
- Ok终于来到BaseDexClassloader的findClass方法,他会遍历所有dexElement,通过clss的名字,加载这个类为class对象
- dexFile加载class的实现是通过native实现的,就这样
类加载热修复原理
经过对PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我们知道,安卓的类加载器在加载一个类时会先从自身DexPathList对象中的Element数组中获取(Element[] dexElements)到对应的类,之后再加载。采用的是数组遍历的方式,不过注意,遍历出来的是一个个的dex文件。在for循环中,首先遍历出来的是dex文件,然后再是从dex文件中获取class,所以,我们只要让修复好的class打包成一个dex文件,放于Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,有bug的class也是存在的,不过是放在了Element数组的最后一个元素中,所以没有机会被拿到而已。