热修复原理浅析(二)

原文链接:https://juejin.im/post/5d492717f265da03d316a985

了解热修复,需要有点预热的知识,先从class文件和dex文件说起

class文件和dex文件

class文件

什么是class文件

他是一种文件格式

简单说,就是能被JVM虚拟机识别、加载、并执行的文件格式

而且除了java语言,还有很多其他语言也可以编译出class文件,当然还有kotlin


上图摘抄自【深入Java虚拟机】之二:Class类文件结构

如何手动编译出一个class文件

很简单
javac hello.java

class文件的作用

记录一个类文件里的所有信息,记住是一个类文件,而且是所有信息

Class类文件结构

详细的可参考【深入Java虚拟机】之二:Class类文件结构

这里简要说一下:

  1. Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。

  2. 下表列出了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文件呢

  1. class文件内存占用大,不适合移动端,最关键就是一个class文件只能表述一个类文件的所有属性
  2. 堆栈的加载模式,加载速度较慢
  3. 文件IO操作多,类查找慢

dex文件

什么是dex文件

能被DVM虚拟机识别、加载、并执行的文件格式

如何手动编译一个dex文件

在build-tools里面找到dx.bat

要使用dx命令,记得配置环境变量

  1. javac命令 生成class文件

javac hello.java

  1. dx命令 生成dex文件

dx --dex --output hello.dex hello.class

  1. adb命令把hello.dex文件放到手机内存卡

adb push hello.dex /storage/emulated/0

  1. 进入shell

adb shell

  1. dalvikvm命令 执行dex文件里的hello方法

    注意dex文件必须在Andriod手机执行,因为手机里才有DVM虚拟机

dalvikvm -cp /sdcard/hello.dex hello

dex文件的作用

一个class文件只是记录一个Java类的所有信息

但是一个dex记录所有类文件的信息,是整个工程的信息

dex文件结构

上图中的文件头部分,记录了dex文件的信息,所有字段大致的一个分部;

索引区部分,主要包含字符串、类型、方法原型、域、方法的索引;

索引区最终又被存储在数据区,其中链接数据区,主要存储动态链接库,so库的信息。

dex文件长什么样子呢

来张大图

一张图理解dex

dex与class异同

当java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右



class与dex异同之处

编年体与纪传体

纪传体通过记叙人物活动反映历史事件的体裁,通过记叙人物活动,反映历史事件。 如:《秦始皇本记.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直接或者间接引用的对象,就不可销毁

引用类型

强软弱虚

弱引用的创建与使用

垃圾回收算法

标记-清除算法

好处:不需要让对象进行移动,仅需要对不存活的对象进行处理,在存活对象较多时候,执行效率高效,但是内存碎片很多

复制算法

好处:当存活的对象比较少时,较为高效,但是需要另外一块空间,用于管理移动

标记-整理算法
  1. 先遍历把可回收对象扫描出来,如B
  2. 扫描清除未标记对象
  3. 把存活的对象,进行移动,没有内存碎片

以上三种算法各有优缺点,虚拟机根据不同情况,采用不同算法,进行垃圾回收

触发回收

  1. jvm无法为新对象创建内存了
  2. 手动调用System.gc()方法(并不会马上执行gc)
  3. 低优先级的gc线程,被运行时就会执行

Dalvik 虚拟机与Jvm异同之处

  1. 执行文件不同,一个是class文件,一个是dex
  2. 类加载系统区别较大
  3. Dalvik 可以同时存在多个,Jvm只能同时存在一个
  4. 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的特点

双亲代理模式

  1. classloader加载字节码时,先询问当前classloader是不是加载过此类,如果加载过,直接返回(不会重复加载字节码)
  2. 如果没有加载过,询问父classloader是不是加载过,如果加载过返回parent加载过的字节码文件
  3. 如果整个继承线路都没加载过这个字节码,才会由子classloader完成加载

由此可见,一个字节码文件被任意一个classLoader加载过,就不会被其他classLoader加载了,提高了加载效率,也带来了另外特性

类加载共享功能

一个字节码文件一旦被顶层classLoader加载过,就会被整个继承体系所共有

类加载隔离功能

不同继承路线的classLoader加载的类,肯定不是同一个类,防止被冒充

例如String这个类,肯定在顶层的classLoader里会把它加载,这样就避免,你自己写个classLoader来篡改string这个类的加载过程

什么样的类才能叫做是同一个类呢

同一个包名+同一个类名+同一个类加载器加载的类,才叫同一个类

ClassLoader的源码

如果都找不到,会走findClass方法,看一下这个方法

那么ClassLoader有哪些子类呢

源码目录

间接子类:DexClassloader

DexClassloader源码查看

上面的第二个参数很重要,这个路径是系统内部的路径,就是因为这个参数,才能去把未安装到app里的dex文件,加载进来

间接子类:PathClassLoader

PathClassLoader源码查看

其实这两个间接地子类,什么也没做,只是一个能加载外部的dex文件,一个只能加载apk内部的文件,主要逻辑还是他们的父类BaseDexClassloader实现的,我们接着看BaseDexClassloader的findClass方法,看看是如何加载dex文件的

直接子类:BaseDexClassloader

BaseDexClassloader源码查看

发现直接调用的是DexpathList的findclass方法

DexPathList源码

Element是类DexPathList的一个内部类,它其中重要的一个变量就是DexFile,就是dex文件。

看看这个Element[]是怎么实现的

来到makePathElement方法

makePathElements方法核心作用就是将指定路径中的所有文件转化成DexFile同时存储到到Element[]这个数组中。nativeLibraryDirectories 就是lib库了。
最终在findclass方法中实现。

接着看看dexFile的loadClassBinaryName方法,我们进入DexFile这个类

DexFile源码查看

回顾一下,我们的源码解析经历了些什么

  1. 首先看了ClassLoder 的双亲委托模式的实现,发现最终指向了findClass()这个方法
  2. 然后发现他是一个空实现,他等着子类去实现
  3. ClassLoader有一个直接子类BaseDexClassloader和两个间接子类DexClassloader 、PathClassLoader其实这两个间接地子类,什么也没做,只是DexClassloader能加载外部的dex文件,PathClassLoader只能加载apk内部的文件,主要逻辑还是他们的父类BaseDexClassloader实现的
  4. 在BaseDexClassloader里发现findClass,调用的是DexPathList的findClass方法
  5. 在DexPathList里,先看到一个Element这个内部类,他里面有个重要的变量叫DexFile,Element[]是通过makePathElement实现的
  6. makePathElement遍历所有文件,把所有dex加载为dexFile,并且存到Element[]里
  7. Ok终于来到BaseDexClassloader的findClass方法,他会遍历所有dexElement,通过clss的名字,加载这个类为class对象
  8. dexFile加载class的实现是通过native实现的,就这样

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章