系统源码剖析

进阶之光笔记三

2021-05-12  本文已影响0人  纵横Top

JNI原理

暂且跳过

Java虚拟机

概述

我们常说的JDK(Java Development Kit)包含了Java语言、Java虚拟机和Java API类库这三个部分,是Java程序开发的最小环境。而JRE(Java Runtime Environment)包含了Java API中的Java SE API子集和Java虚拟机这两部分,是Java程序运行的标准环境。
Java虚拟机是一个家族,实际上有很多类型的Java虚拟机,有几个主流的:
1.HotSpot VM : Oracle JDK和 OpenJDK中自带的虚拟机,是最主流的和使用范围最广的Java虚拟机。一般不做特殊介绍的话,我们所说的虚拟机就是这一款,当下属于Oracle公司。
2.J9 VM:IBM开发的虚拟机,目前是其主力发展的Java虚拟机。
3.Zing VM :以Oracle的HotSpot VM为基础,改进了许多影响延迟的细节。
Android中的Dalvik和ART虚拟机并不属于Java虚拟机

Java虚拟机执行流程

0FCDFDF6DFA46E12762975EDDED7BBF5.jpg
如上图,java虚拟机执行流程分为两大部分:
①编译时环境
②运行时环境
当一个Java编译器编译后会生成Class文件。Java虚拟机与Java语言并没有必然联系,它只与特定的二进制文件:Class文件相关。因此无论任何语言只要能够编译成Class文件,就可以被Java虚拟机识别并执行。
比如Java、Kotlin、Groovy等。

Java虚拟机结构

Java虚拟机结构包括运行时数据区域、执行引擎、本地库接口和本地方法库。
Class文件格式
Java文件被编译后生成了Class文件,这种二进制格式文件不依赖于特定的硬件和操作系统。每一个Class文件中都对应着唯一的类或者接口的定义信息,但是类或者接口并一定定义在文件中,比如类和接口可以通过类加载器来直接生成。

类的生命周期

一个Java文件被加载到Java虚拟机内存中到从内存中卸载的过程被称为类的生命周期。类的生命周期包括的阶段分别是:加载、链接、初始化、使用和卸载。其中链接分为三个部分:验证、准备和解析。因此类的生命周期分为7个阶段。而从广义来上来,类的加载包含了类的生命周期的5个阶段:加载、链接(验证、准备、初始化)、初始化。其五个阶段的工作如下:
(1)加载:查找并加载Class文件
(2)链接:包括验证、准备和解析
验证:确保被导入类型的正确性
准备:为类的静态字段分配字段,并使用默认值初始化这些字段
解析:虚拟机将常量池内的符号引用替换为直接引用。
(3)初始化:将类变量初始化为正确初始值。

类加载子系统

类加载子系统通过多种类加载器来查找和加载Class文件到Java虚拟机中,Java虚拟机有两种类加载器:系统加载器和自定义加载器。
系统加载器分为三种:
①Bootstrap ClassLoader(引导类加载器)
用C/C++实现的加载器,用于加载执行的JDK的核心类库。比如java.lang,java.uti等。Java虚拟机的启动就是通过引导类加载器来创建一个初始类完成的。因为此加载器是使用与平台无关的C/C++语言实现的,所以java访问不到。
②Extensions ClassLoader(扩展类加载器)
加载java的扩展类,比如java.ext.dir下的。
③Application ClassLoader(应用程序类加载器)
又称System ClassLoader,这个类加载器可以通过ClassLoader的getSystemClassLoader方法获取到。继承自java.lang.ClassLoader。

运行时数据区域(【Java内存】)

很多人将java内存区域分为堆内存(Heap)和栈内存(Stack),这种方法不够准确。
根据《Java虚拟机规范(Java SE7版)》,将内存区域分为以下几个部分:
程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区,下面一一介绍:
1.程序计数器
程序计数器也叫作PC寄存器,是一块较小的内存空间。Java虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,在一个确定的时刻只有一个处理器执行一条线程中的指令。为了在线程切换后能恢复到正确的执行位置。每个线程都会有一个独立的程序计数器。程序计数器是线程私有的。
也是java虚拟机规范中唯一没有规定任何OutOfMemoryError情况的数据区域。
2.Java虚拟机栈
每一条Java虚拟机线程都有一个线程私有的Java虚拟机栈。它的生命周期与线程相同,与线程是同时创建的。存储的内容是方法调用的状态(就是方法的内容),包括局部变量、参数、返回值以及运算的中间结果等。
3.本地方法栈
与虚拟机栈类似,只不过本地方法栈是用来支持Native方法的。
4.Java堆
线程共享的运行时内存区域,占用区域最大。Java堆用来存放对象实例,几乎所有的对象实例都会在这里分配内存。是GC机制管理的主要部分,也被称为GC堆。Java堆的容量可以是固定的,也可以动态扩展。
堆分为三个部分:
①年轻代:又被划分为Eden区和Survivor(From Survivor、ToSurvivor),空间分配比例为8:1:1.
②老年代
5.方法区
方法区是线程共享的运行时内存区域,用来存储已经被java虚拟机加载的类的结构信息,包括运行时常量池、字段和方法信息、静态变量等信息。注意【运行时】

对象的创建

创建一个对象会进行如下操作:
①判断对象对应的类是否加载、链接和初始化
②为对象分配内存
根据java堆是否完整,分为两种方式:指针碰撞(内存是规整的)、空闲列表(内存不规整)
③处理并发安全问题
④初始化分配到的内存空间
将分配到的内存信息,除了对象头之外都初始化为零
⑤设置对象的对象头
⑥执行init方法进行初始化
执行init方法,初始化对象的成员变量、调用类的构造方法,这样一个对象就被创建出来了。

对象的堆内存布局

emm...

oop-klass模型

用来描述java对象实例的一种模型。剩下的就emm...

垃圾标记算法

目前有两种垃圾标记算法:引用计数法和根搜索算法(可达路径法)
java中有四种引用:强引用、软引用、弱引用和虚引用。
1.强引用:新建一个对象时就创建了一个具有强引用的对象,也就是new出来的对象,这种引用的对象,垃圾收集器绝不会回收它。
2.软引用:当内存不够时,会回收这些对象的内存。对应SoftReference。
3.弱引用:比软引用具有更短的生命周期,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存是否足够,都会回收它的内存,对应WeakReference.
4.虚引用:虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,这就和没有任何引用一样,在任何时候都可能被垃圾收集器回收。对应PhantomReference类。
两种标记算法:
1.引用计数法
其基本思想就是每个对象都有一个引用计数器,当对象在某处被引用的时候,它的引用计数器就加1,引用失效时就减1.当值为0时,该对象就不能被使用,变成了垃圾。
目前主流的Java虚拟机没有选择引用计数算法来标记垃圾,是因为没有解决对象之间相互循环引用的问题。

2.根搜索算法
这个算法的基本思想就是选定一些对象作为GC Roots,并组成根对象集合,然后以这些GC Roots的对象作为起始点,向下搜索,如果目标对象到GC Roots是连着的,我们称该目标对象是可达的,如果目标对象不可达则说明目标对象时可以被回收的对象。解决了计数算法无法解决的问题:已经死亡的对象因为相互引用而不能被回收。 在Java中,可以作为GC Roots的对象主要有以下几种:
①Java栈中引用的对象
②本地方法栈中JNI引用的对象
③方法区中运行时常量池引用的对象
④方法区中静态属性引用的对象
⑤运行中的线程
⑥由引导类加载器加载的对象
⑦GC控制的对象

被标记为不可达的对象会立即被垃圾回收器回收吗?

Java对象在虚拟机中的生命周期

java对象被类加载器加载到虚拟机中后,在java虚拟机中有7个阶段:
①创建阶段
②应用阶段
③不可见阶段
在程序中找不到对象的任何强引用,但仍可能被特殊的强引用GC Roots持有着,比如对象被本地方法栈中JNI引用或被运行中的线程引用等。
④不可达阶段
垃圾回收器发现对象不可达
⑤收集阶段
⑥终结阶段
等待垃圾收集器回收该对象空间。
⑦对象空间重新分配阶段

垃圾回收算法

1.标记-清除算法
分为两个阶段:
标记阶段:标记出可以回收的对象
清除阶段:回收被标记的对象所占用的空间
缺点:标记和清除的效率都不高,容易产生大量不连续的内存碎片。
2.复制算法
将内存空间划为两个相等的区域,每次只使用其中一个区域。在垃圾收集时,遍历当前使用的区域,将存活的对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
缺点:使用内存为原来的一半。如果存活对象很少,复制算法的效率就很高,但绝大多数对象的生命周期都很短,并且这些生命周期很短的对象都存于新生代中,所以复制算法被广泛应用于新生代中。

  1. 标记-压缩算法
    与标记-清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界以外的内存进行回收。解决了内存碎片的问题,被广泛应用于老年代中。
分代收集算法

java堆区中大部分的对象生命周期很短,少部分对象生命周期很长。应该对不同生命周期的对象采取不同的收集策略,根据生命周期长短将他们放到不同的内存区域,并在不同的区域采用不同的收集方法,这就是分代的概念。现在主流的Java虚拟机的垃圾收集器都是采用的分代收集算法。Java堆区基于分代的概念,分为新生代和老年代。其中新生代再细分为Eden空间、From Survivor空间和 To Survivor空间。Eden空间中觉大多数对象生命周期很短,所以新生代的空间划分并不是均匀的,HotSpot虚拟机默认Eden空间和两个Survivor空间的所占的比例为8:1.
分代收集中垃圾收集的类型分为两种:
①新生代垃圾收集
②老年代垃圾收集,通常情况下会伴随至少一次的新生代垃圾收集,它的收集频率较低,耗时较长。

当执行一次新生代垃圾收集时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次新生代垃圾收集并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间。有两种情况Eden空间和From Survivor空间存活的对象不会被复制到To Survivor空间,而是晋升到老年代。一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold所指定阈值。另一种是To Survivor空间容量达到阈值。当所有存活的对象都会复制到To Survivor空间或晋升到老年代,也就意味着Eden空间和From Survivor空间剩下的都是可回收的对象。这个时候GC执行新生代垃圾收集,Eden空间和From Survivor空间都会被清空,新生代中存活的对象都在To Survivor中。接下来From Survivor和To Survivor空间互换位置,每次Survivor空间互换都要保证To Survivor是空的,这就是复制算法在新生代中的应用。
老年代则会采用标记-压缩算法或者标记-清除算法。(毕竟收集频率低,耗时长)

Dalvik和ART

Dalvik虚拟机,简称Dalvik VM或者DVM,Android 4.4及之前默认采用的还是DVM,4.4提供了一个选项开启ART.在Android 5.0版本中被舍弃,改而使用ART,DVM退出历史舞台。

DVM与JVM区别

DVM之所以不是一个JVM,主要原因是DVM没有遵循JVM来实现,DVM与JVM主要区别如下:
1.架构不同
JVM基于栈,需要去栈中读写数据;
DVM基于寄存器;
2.执行的字节码不同
JVM会通过.class文件和jar文件获取相应的字节码。
DVM会用dx工具将所有的.class文件转换为一个dex文件,然后从该.dex文件中读取指令和数据。
3.DVM允许在有限的内存中同时运行多个进程
4.DVM由Zygote创建和初始化
5.DVM有共享机制,JVM不存在共享机制
6.DVM早期没有使用JIT编译器

ART和DVM的区别

下面是重点,需要背诵的!
1.DVM应用每次运行时,字节码都需要通过JIT编译器编译为机器码,会使应用程序的运行效率变低。而ART中,系统在安装应用程序时会进行一次AOT(预编译),将字节码先编译成机器码并存储在本地。以空间换时间。Android 7.0版本,ART加入了即时编译器JIT,这样应用程序安装时并不是将字节码全部编译成字节码,而是在运行中将热点代码编译成机器码,节省了应用程序的安装时间并节省了存储空间。
2.DVM是为32位CPU设计的,而ART支持64位并兼容32位CPU,这也是DVM被淘汰的主要原因之一。
3.ART对垃圾回收机制进行了改进,更频繁地执行并行垃圾回收,将GC暂停由2次减少为1次等。
4.ART的运行时堆空间划分和DVM不同。
ART的运行时堆默认是由4个Space和多个辅助数据结构组成的;DVM有两个Space,分别是Zygote Space、Allocation Space;ART新增了两个:Image Space和Large Object Space。前者存放一些预加载类,后者用来分配一些大对象。
此外,ART的GC日志和DVM不同,ART会为那些主动请求的垃圾收集事件或者认为GC速度慢时才会打印GC日志,GC速度慢指的是GC暂停超过5ms或者GC持续时间超过100ms。

引起GC的原因
原因很多,只例举几个
①Concurrent:并发GC,不会使App的线程暂停,该GC在后台运行,不会阻止内存分配。
②Alloc:堆内存已满时,App尝试分配内存引起GC
③Explict:App显式的请求垃圾收集,例如调用System.gc()
④NativeAlloc:Native内存分配时,比如为Bitmap分配内存
等等。

ART & DVM启动时机

Zygote在启动时会调用app_main.cpp文件中的main方法;
main方法中会调用AppRuntime的start函数,其实现在其父类的AndroidRuntime中,其中会调用startVm方法来创建Java虚拟机。然后会调用jin_invocation的init函数,其中调用getLibrary方法来加载ART,传参为libart.so说明用的是ART,传参为libdvm.so说的用的是DVM.

Binder原理

Binder设计与实现
Binder图文详解
根据Android系统的分层,Binder机制可以分为:
Java Binder
Native Binder
Kernel Binder
其中Native Binder指的是Native 层的Binder。

学习Binder的前置知识点

本小节主要有三部分:
①Linux 和 Binder的IPC通信原理
②使用Binder的原因
③学习Binder的原因

Linux和Binder的IPC通信原理

先附上一张进程通信简单模型


QQ图片20210406224215.jpg

为了保护用户进程,不能直接操作内核,以保证内核的安全,操作系统从逻辑上将虚拟空间划分为:用户空间、内核空间。
内核空间是Linux内核的运行空间,用户空间是用户程序的运行空间。为了保证内核的安全,他们是隔离的,即使用户程序崩溃了,内核也不会受影响。内核空间的数据数据是可以进程间共享的,而用户空间的数据则不可以。
进程隔离指的是一个进程不能直接操作或者访问另一个线程,也就是进程A不能直接访问进程B的数据。
系统调用:
用户空间如果需要访问内核空间,只有一种方式,那就是:系统调用。
进程A和进程B的用户空间可以通过如下系统函数和内核空间交互::
copy_from_user:将用户空间的数据复制到内核空间
copy_to_user:将内核空间的数据复制到用户空间
内存映射
应用程序不能直接操作设备硬件地址,所以操作系统提供了一种机制:内存映射,将设备地址映射到进程虚拟内存区。
如果不用内存映射,就需要在内核空间新建一个页缓存,页缓存区复制磁盘中的文件,然后用户空间再去复制页缓存的中文件,这就需要两次复制。而如果采用内存映射,映射模型如下:

QQ图片20210406225630.jpg
由于新建了虚拟内存区域,磁盘文件和虚拟内存区域就可以直接映射,少了一次复制。
在Linux中通过系统调用函数mmap函数来实现内存映射。将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改就可以直接反映到内核空间。
Linux IPC通信原理

内核程序在内核空间分配内存并开辟一块内核缓存区,发送进程通过copy_from_user()函数将数据复制到内核空间的缓冲区中。同样,接收进程接收数据时在自己的用户空间开辟了一块内核缓存区,然后内核程序调用copy_to_user()函数将数据从内核缓存区复制到接收进程。这样就是一次Linux进程间通信。
Linux的IPC通信有两个问题:
①一次数据数据传递需要经历:用户空间->内核空间->用户空间,需要两次数据复制。
②接收进程不知道需要多大的空间存放将要传递过来的数据,只能开辟尽可能大的空间,或者调用API接收消息头获取消息体的大小,浪费了空间或者时间。
Binder通信原理

Binder基于内存映射。
Binder通信步骤如下:
①Binder驱动在内核空间创建一个数据接收缓存区。
②在内核空间开辟一块内核缓存区,建立内核缓存区与数据接收缓存区之间的映射关系。
③发送方通过copy_from_user()函数将数据复制到内核缓存区中,由于内核缓存区与数据接收缓存区也就是接收进程的用户空间存在内存映射。因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

整个过程只使用了一次复制。

使用Binder的原因

1.性能方面:
性能方面主要影响是数据复制次数。Socket/管道/消息队列都复制了两次,共享内存不需要复制,Binder复制一次,性能仅次于共享内存。(共享内存有很多缺点,下面介绍)
2.稳定性方面:
Binder基于C/S架构,这个架构采用两层结构,技术上已经十分成熟了。共享内存没有分层,难以控制,还有可能产生死锁。[不用共享内存原因之一]
3.安全方面
传统的IPC接收方无法获得对象可靠的用户进程ID(UID)/进程ID(PID).无法鉴别对身份。
4.语言方面
Linux是基于C语言的,C语言是面向过程的。而Java是面向对象的。Binder本身符合面向对象的思想。

系统中并不是所有进程通信都是用了Binder,而是根据场景来选择最合适的,比如Zygote进程与AMS通信采用的Socket,Kill Process采用的是信号。

学习Binder的原因(Binder的作用)

Binder在Android中的作用举足轻重,很多原因都与Binder 相关。
①系统中的各个进程是如何通信的
②Android启动流程
③AMS/PMS原因
④四大组件的原理、比如Activity如何启动
⑤插件化原理
⑥系统服务的客户端和服务端是如何通信的。(比如MediaPlayer和MediaPlayerService)
SystemServer进程启动后会创建Binder线程池,其目的是通过Binder可以使SystemServer进程中的服务能够和其他进程间通信。

ServiceManager中的Binder机制

Binder框架中定义了四个角色:Binder驱动、Client、Server、ServiceManager(用于管理系统中的服务)。
Binder驱动:
和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。
接下来是第二篇参考文章的分析,更加明了,重点来了:
定义
一种虚拟设备驱动
作用
连接Service进程、Client进程和ServerManager进程的桥梁。
Binder驱动职责:
①创建了一块内存缓存区
②实现了内存映射关系:将内核缓存区和接收进程内存缓存区映射到同一个内存映射缓冲区中(调用mmap()函数)

Binder驱动工作原理

(1)注册服务
①Server进程向Binder驱动发送服务注册请求
②Binder驱动将注册请求转发给ServiceManager进程
③ServiceManager进程添加该Server进程(即已注册该服务)
(2)获取服务
①Client向Binder驱动发起获取服务的请求,传递要获取的服务名称
②Binder驱动将该请求传递给ServiceManager进程
③ServiceManager查找到Client需要的Server对应的服务信息
④通过Binder驱动将上述服务信息返回给Client进程
(3)使用服务
①Binder驱动为跨进程做做准备,调用mmap函数进行内存映射
②Client进程通过Binder驱动将参数信息传递给Server进程
③Server进程通过Client进程要求调用目标方法
④Server进程将运行结果返回给Client进程

image.png
有一点需要注意:
Client进程、Server进程和ServiceManager进程通信都必须通过Binder驱动(使用open()和ioctl()函数),而非直接交互
Client进程、Server进程 & Service Manager进程属于进程空间的用户空间,不可进行进程间交互
Binder驱动 属于 进程空间的 内核空间,可进行进程间 & 进程内交互
还有一点也需要注意:
Binder驱动和ServiceManager属于Android基础架构,是已经实现好的;
而Client和Server属于应用层,需要客户端自己实现。

ServiceManager
以MediaPlayer为例理解ServiceManager。在Android系统启动时,MediaServer也被启动。在MediaServer的main方法中,会获得ProcessState实例。然后获取到IServierManager,通过IServiceManager,其他进程就可以和当前的ServiceManager交互,交互方式就是Binder通信。ProcessState实例代表进程的状态,是一个单例(保证每个进程只有一个ProcessState实例),创建参数是dev/binder,也就是Binder驱动。在其构造方式中,打开了Binder驱动,并调用mmap()函数。mmap函数会在内核虚拟地址空间中申请一块与用户空间内存相同大小的内存,然后申请物理内存,将同一块物理内存分别映射到内核虚拟地址空间和用户虚拟内存空间中,实现内核虚拟空间和用户虚拟内存空间数据同步操作,也就是内存映射。在打开Binder驱动后,会调用ioctl()函数和Binder设备进行参数的传递,并且将Binder支持的最大线程数设定为15.
总的来说,ProcessState实例有两个重要作用:
①打开/dev/binder设备也就是Binder驱动并设定Binder支持的最大线程数。
②通过mmap()函数为Binder分配一块虚拟内存空间,达到内存映射的目的。

ServiceManager中的Binder机制

ServiceManager中不但使用了Binder通信,而且它本身就是Binder体系内的。
(1)在ServiceManager中创建了BpBinder,BpBinder和BBinder和Binder通信的"双子星",BpBinder是客户端与服务端交互的代理类,而BBinder则代表了服务端。BpBinder和BBinder是一一对应的,BpBinder会通过handle来找到对应的BBinder。
其通信流程图如下:


QQ图片20210407213529.jpg

(2)BpBinder和BBinder负责Binder的通信,而IServiceManager用于处理ServiceManager的业务。IServiceManager继承IInterface,其内部定义了一些常量和一些操作Service的操作。其具体实现是BpServiceManager,此类通过Binder来实现通信。
(3)简单总结:
BpBinder和BBinder都和通信有关,他们都继承自IBinder。
BpServiceManager派生自IServiceManager,他们都和业务有关。
Native Binder的原理的核心就是ServiceManager的原理。

Binder机制在Android中的具体实现原理

Binder机制在Android中的具体实现主要依靠Binder类,其实现了IBinder接口。
TODO://手撸Binder代码,写一个简单的Demo。

理解ClassLoader

DVM和ART加载的是dex文件,而JVM加载的是.class文件,因此它们的类加载器ClassLoader是有区别的。

Java中的ClassLoader

类加载子系统的主要作用就是通过多种类加载器(ClassLoader)来查找和加载Class文件到Java虚拟机中。
ClassLoader的类型:
Java中的类加载器主要有两种类型:
①系统类加载器
②自定义类加载器
系统类加载器包括3种:
①Bootstrap ClassLoader
②Extensions ClassLoader
③Application ClassLoader

1.Bootstrap ClassLoader(引导类加载器)
Java VM的启动就是通过Bootstrap ClassLoader创建一个初始类来完成的。由于Bootstrap ClassLoader是使用C/C++语言实现的,所以该类加载器不能被Java代码访问到。Bootstrap ClassLoader并不继承java.lang.ClassLoader
2.Extensions ClassLoader(扩展类加载器)
Java中的实现类是ExtClassLoader,用于加载Java的扩展类,提供除系统类之外的额外功能。加载内容包括系统属性java.ext.dir所指定的目录;父jiazai
3.Application ClassLoader(应用程序类加载器)
Java中的实现类为AppClassLoader,也被称为System ClassLoader,因为其可以通过ClassLoader的getSystemClassLoader方法获取到。父类是ExtClassLoader
4.Custom ClassLoader(自定义类加载器)
java还有其他的一些类加载器,比如SecureClassLoader,继承自ClassLoader,扩展了权限方面的功能,加强了ClassLoader的安全性。URLClassLoader继承自SecureClassLoader,可以通过URL路径从jar文件和文件夹中加载类和资源。

双亲委托机制

类加载器查找Class所采用的是双亲委托机制。就是先判断Class是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样一次递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接返回,如果没有找到,则继续依次向下查找,如果还没有找到则最后交给自身去查找。
有以下两点好处:;
①避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是直接读取已经加载的Class。
②更加安全,如果不用双亲委托,就可以自定义一个String类用自己的类加载器来替代系统的String类,显然会造成安全隐患。

自定义类加载器

只需要两个步骤:
(1)定义一个自定义ClassLoader并集成抽象类ClassLoader
(2)复写findClass方法,并在findClass方法中调用defineClass方法。

Android中的ClassLoader

继承关系如下图:


image.png

Java中的ClassLoader可以加载jar文件和Class文件(本质是加载Class文件),这一点在Android中并不适用,因为Android中加载的不是class文件,而是dex文件。
Android中的ClassLoader也分为两种:
系统类加载器和自定义加载器。
系统类加载器分为三种:
BootClassLoader/PathClassLoader/DexClassLoader

1.BootClassLoader
Android系统启动时会使用PathClassLoader而不是BootClassLoader来预加载常用类,与SDK中的BootstrapClassLoader不同,它并不是C/C++代码实现的,而是由Java实现的。这是一个单例类,修饰符是默认的,只有包内可以访问到。
是PathClassLoader的父类,是ClassLoader的静态内部类;我们的Class文件都是PathClassLoader加载的。如果没有传ClassLoader,默认采用BootClassLoader,比如Class.forName中的实现。
2.DexClassLoader
DexClassLoader可以加载dex文件以及包含dex的压缩文件(apk和jar文件)。
DexClassLoader的参数有四个:
①dexPath:dex相关文件路径集合,多个路径用分隔符":"分隔
②optimizedDirectory:解压的dex文件存储路径(8.0之后就已经没有用了)
③librarySearchPath:包含C/C++库的路径集合,多个路径用文件分隔符分隔,可以为null。
④parent:父加载器
3.PathClassLoader
Android系统使用PathClassLoader用来加载系统类和应用程序的类。没有参数optimizedDirectory,无法定义加载的dex文件存储路径,所以用来加载已经安装的dex文件。但是8.0之后,optimizedDirectory参数已经没有用了!!!!传到BootClassLoader后并没有继续传入DexFile文件中,所以8.0之后,DexClassLoader和PathClassLoader作用已经一样了!!!
DexClassLoader和PathClassLoader的父类均是BaseDexClassLoader

ClassLoader的加载过程

Android的ClassLoader同样遵循了双亲委托机制,ClassLoader的加载方法是loadClass方法。其中会先检查类是否已经加载,如果已经加载就返回该类,如果没有加载就判断父加载器是否存在,存在就调用父加载器的loadClass方法,委托给父类检查是否加载。如果父加载器没有加载,就调用findClass加载类。findClass交由子类实现:


image.png

其内部调用了DexPathList的findClass的方法,DexPathList是BaseDexClassLoader的构造方法中构造的。
DexPathList中有个重要的数组:dexElements,数组承载的元素是Element,这是DexPathList中定义的静态内部类,内部封装了DexFile,可以理解为一个Element对应一个DexFile,对应一个dex文件。
ElementDexList中就是遍历所有的Element,调用Element的findClass方法去加载每一个dex文件。此findClass方法最终会调用到:


image.png
image.png
native方法defineClassNative来加载dex相关文件。到了native的话,就先到此为止。
BootClassLoader的创建

需要从Zygote进程说起,ZygoteInit的main方法中调用了preload方法

image.png
preload方法中又调用了preloadClasses方法:
image.png
preloadClasses方法用于Zygote进程初始化时预加载常用类。
image.png
这个方法中会将/system/etc/preloaded-classes文件封装成FileInputStream,preloaded-classes文件中存有预加载类目录。接着将输入流包装成BufferedReader,读取所有预加载类的名字。每读一行,就调用Class.forName,通过反射来创建预加载类,看一下forName的具体实现:
image.png
forName中会调用BootClassLoader.getInstance创建BootClassLoader的单例(懒汉模式,静态加锁,全局唯一)。接着调用的classForName是native方法,由c/c++实现。
image.png image.png

简而言之:BootClassLoader是在Zygote进程的Zygote入口方法中被创建的,用于加载preloaded-classes文件中存有的预加载类。

PathClassLoader的创建过程

PathClassLoader是在SystemServer进程中采用工厂模式创建的。

第十三章 热修复原理

热修复框架虽然很多,但其核心技术主要有三类:
代码修复、资源修复和动态链接库修复(so库)

资源修复

很多热修复框架的资源修复都参考了Instant Run的资源修复的原理(Instant Run在Android Studio 3.5已经废除,取而代之的是Apply Changes),Instant Run是AS 2.0新增的运行机制。
Instant Run部署有三种方式,Instant Run会根据代码的情况来决定采用哪种部署方式。无论哪种方式都不需要重新安装App。三种部署方式如下:
①Hot Swap:效率最高的部署方式,不需要重启App,也不需要重启当前的Activity。修改一个现有方法中的代码时会采用Hot swap。
②Warm Swap:App不需要重启,但是Activity需要重启。修改或删除一个现有的资源文件时会采用Warm Swap。
③Cold Swap:App需要重启,但是不需要重新安装。采用Cold Swap的情况很多,比如添加、删除或者修改一个字段和方法、添加一个类等。

instant run的资源修复原理

Instant Run资源修复的核心逻辑在MonkeyPatcher的monkeyPatchExistingResources(Context context,String externalResourceFile,Collection<Activity> activities)方法中。Instant Run资源热修复可以简单的分为两个步骤:
(1)创建新的AssetManager,通过反射调用addAssetPath方法加载外部的资源,这样新创建的AssetManager就含有了外部资源。
(2)将AssetManager类型的mAsset字段的引用全部替换为新创建的AssetManager。(遍历所有的Activity,拿到每一个activity的Resources类里的mAsset然后替换为新创建的AssetManager)

代码修复

代码修复主要有三个方案,分别是:
底层替换方案、类加载方案和Instant Run方案

(1)类加载方案
类加载方案基于Dex分包方案。
Dex分包是因为有两个限制:
①65535限制:
应用中引用的方法数不能超过65535。( 单个Dex文件中,method个数采用使用原生类型short来索引,即2个字节最多65536个method,field、class的个数也均有此限制)
②LinearAlloc限制
在安装应用的时候可能会提示INSTALL_FAILED_DEXOPT,产生的原因就是LinearAlloc限制,DVM的LinearAlloc是一个固定的缓存区,当方法数超过缓存区的大小时会报错。
为了解决上面两个限制,从而产生了Dex分包方案。
Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex。
Dex分包方案主要有两种:
①Dex自动拆包(Google官方方案)
②动态加载方案
主要看动态加载方案。

之前说过ClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass方法,然后调用Element的findClass方法,Element中封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。多个Element组成了有序的Element数组dexElements。当要查找类的时候,会遍历Element数组dexElements(相当于遍历dex文件数组),Element的findClass方法会调用DexFile的loadClassBinaryName方法查找类,如果在Element(dex文件)中找到该类就返回,如果没有就接着在下一个Element中进行查找。

现在我们就可以模拟这个加载流程,我们将有Bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样就首先找到Patch.dex中的Key.class去替换之前存在Bug的Key.class.排在数组后面的dex文件中存在bug的Key.class根据ClassLoader的双亲委托机制就不会被加载,这就是类加载方案。

类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启?这是因为类是无法被卸载的,要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。

虽然都是采用类加载方案,但是具体的实现方案还是有一定区别的。比如 QQ空间的超级补丁和Nuwa是和上面一样将补丁包放在Element数组的第一个元素得到优先加载。微信Tinker是将新旧Apk做了diff,得到patch.dex,再将patch.dex与手机中的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组中的第一个元素。
采用类加载方案的主要是腾讯系为主。

底层替换方案

与类加载方案不同,底层替换方案不会再次加载新类,而是直接在Native层修改原有类。这种方案限制比较多,且不能增减原有类的方法和字段,如果增加了方法数,那么方法索引数也会增加,这样访问方式时,就不能通过索引找到正确的方法。
底层替换方案和反射的原理有些关联,这里拿方法反射来说,方法反射可以调用java.lang.Class.getDeclaredMethod获取对应方法,调用的invoke方法是native方法,其本质是替换Native的ArtMethod结构体中的字段或者整个ArtMethod结构体,这就是底层替换方法的底层实现。这种实现由比较大的局限性,就是各个厂商可能会修改ArtMethod结构体,导致方法替换失败。Sophix采用的是直接替换整个ArtMethod结构体,这样就不存在兼容问题。底层替换方法直接替换了方法,可以立即生效不需要重启。
采用底层替换方案主要以阿里系为主,包括AndFix、Dexposed、Sophix、阿里百川。

Instant Run方案

这种方案主要借助ASM(还有Aspectj)这个字节码操控框架,它能够动态生成类或者增强现有类的功能。可以直接产生class文件,也可以在类被加载到虚拟机之前动态改变类的行为。

动态链接库的修复

Android平台的动态链接库主要就是指so库,热修复框架so的修复主要就是重新加载so,因此so的修复的基础原理就是加载so。
加载so主要用到的就是System类的load和loadLibrary方法。目前so的修复都是基于这两个方法。
load方法和loadLibrary方法最终都会调用native层的nativeLoad方法。nativeLoad加载so库的最终实现是LoadNativeLibrary函数,此函数主要做了三个工作:
①判断so是否被加载过,两次ClassLoader是否是同一个,避免so重复加载。
②打开so并得到so句柄,如果so句柄获取失败,就返回false。
③查找JNI_OnLoad函数指针,根据不同情况设置was_successful的值,最终返回该值,通知上层是成功还是失败。
总结一下,so修复的两个方案:
①将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载。
②调用System的load方法来接管so的加载入口。

第十四章 Hook技术

Hook技术主要用到逆向工程中。逆向分析分为静态分析和动态分析。静态分析指的是一种在不执行程序的情况下对程序行为进行分析的技术;动态分析是指在程序运行时对程序进行调试的技术。Hook技术就属于动态分析。


QQ图片20210413012301.jpg

正常的A调用B,B处理后将结果返回个A.而Hook技术可以拦截事件。这里的Hook可以是一个方法或者一个对象,它想钩子一样挂在A和B之间。当A调用B,B返回结果给A。Hook就可以在中间起到“欺上瞒下”的作用。
为了保证Hook的稳定性,Hook点一般选择容易找到并且不易变化的对象,静态变量和单例就符合这一条件。(因为源码容易改变,而单例和静态变量因为引用的地方较多,所以不容易被sdk更新迭代掉。)

Hook的分类

(1)按照API语言划分
Hook Java :主要通过反射和代理实现,应用SDK开发环境中修改Java代码。
Hook Native:应用于NDK开发,修改Native代码。
(2)进程划分
应用程序Hook:只能Hook当前所在的应用程序进程
全局Hook:对Zygote进行Hook,可以实现Hook系统所有的应用程序进程。(因为应用程序进程都是Zygote fork出来的。)
(3)实现划分
通过反射和代理实现,只能Hook当前的应用程序进程。
通过Hook框架实现,比如Xposed,可以实现全局Hook,但是需要root。

插件化原理

1.Activity插件化

Activity插件化视频地址
![QN`XCC$AEGS)5M0JXT4O.png
要想跳转插件里的Activity,如果我们通过正常的startActivity去跳的话,是可行的,因为跳转的时候,AMS会检测要跳转的Activity是不是我们项目中清单文件中注册的Activity,如果不是则会抛出异常。(主要是没有注册插件的Activity,也没办法跨项目注册)
而插件化的思想就是检测的时候,我们将插件的Activity替换成我们项目中代理的一个Activity,在通过校验后,再将代理Activity替换称为我们插件的Activity,这就是Activity插件化的基本思想。(注意前后要保留intent中参数)
Activity插件化主要有3种实现方式:
①反射实现
②接口实现
③Hook技术实现
反射会对性能有影响,目前主流的插件化框架没有采用这种方式。
接口的可以参照dynamic-load-apk源码。
这里我们就可以通过Hook技术来实现这种"偷天换日"的操作。这里就要用到两次Hook技术。目前有两种方案:
①Hook IActivityManager来实现。
②Hook Instrumentation实现。
先跟着视频看Hook IActivityManager实现:

image.png
我们可以借助Intent,setComponet方法来进行隐式跳转。而如果想把PluginActivity替换成ProxyActivity,实际上只要把Intent的packgeName和ActivityName替换成代理Activity的包名和类名就可以。
我们我们的目的就是找到方便替换intent的地方。
接下来我们就查一下源码,看看到底哪些地方适合修改intent:
image.png
image.png
这两个地方都可以替换,第一个点就是替Instrumentation;第二个点就是替换IActivityManager;第二张图可以看到调用了AMS,而且这里还是一个静态的,这里就是用使用动态代理去替换intent。
代理的就是这里的startActivity方法。这里我们就需要创建一个对象,替换掉ActivityManager.getService()这个对象。
image.png
这里就可以用动态代理来进行操作了,
image.png
接下来还要用这个代理的对象替换系统这个对象;这个稍后补充;我们先做一下invoke里替换intent的操作:
image.png
简单说一下第一步Hook对intent的处理;
①需要遍历startActivity方法,拿到我们所需要的intent的参数;这个intent是插件的intent,因为我们第一步Hook的目的就是将插件的intent替换成代理的intent,以通过AMS的检查
②新建代理的intent,设置代理的包名和类名,并替换掉startActivity的intent参数
③需要将原有的intent也就是插件的intent保留,因为我们第二步Hook需要将intent替换回插件的intent,这样我们才可以跳转到插件的activity中,这样原有intent里的内容就不会丢失,putExtra放进去。

下面就是用动态代理的这个对象替换掉系统中原有的IActivityManager对象,这样我们才能执行到这个动态代理对象的逻辑,所以我们就需要找到系统中这个IActivityManager对象的field,通过反射拿到,再用动态代理替换掉。

image.png
所以我们要替换掉的就是这里的ActivityManager.getService()
image.png
可以看到这个对象获取是在:
IActivityManagerSingleton.get();这个方法中,我们看一下这个方法:
image.png
可以看到我们需要做的就是拿到mInstance,然后替换掉它。
要拿mInstance,就要拿到这个Field,因为是非静态的,所以我们还需要拿到这个字段的对象,也就是Singleton对象:
image.png
那么我们就要看Singleton对象是在哪创建的,回到上一步:
image.png
这个IActivityManagerSingleton就是我们的Singleton对象,而且这还是这个静态的对象!(当然还是要通过ActivityManager这个类)
image.png
接下来我们就是具体获取的代码了,分以为几步:
①通过反射获取到ActivityManager对象
②拿到该类中的IActivityManager对象,因为是静态的,所以Field get的时候直接传入null即可
③拿到Singleton类
④拿到对应的Field也就是mInstanceField,因为这个是非静态的,我们需要传入具体对象,也就是IActivityManager对象,从而拿到mInstance
(这四步就是为了拿到系统的IActivityManager对象)
⑤拿到系统的mInstance对象后,我们invoke方法中的method.invoke的第一个参数就可以传入mInstance
⑥这时候,我们的反射和代理都做完了,接下来就是将代理对象替换掉我们系统的对象,也就是将mInstanceField的值由singleton对象替换称为我们的代理对象。
image.png
image.png
image.png

第一步将PluginActivity替换称为ProxyActivity以跳过AMS的检查我们已经做好了。下面就是第二步,在检测过后打开Activity的时候,再将ProxyActivity替换称为我们的PluginActivity。
这里就涉及到Activity的启动流程了:

image.png
首先调用ActivityThread里的handleLaunchActivity,因为要替换intent,所以我们性需要找到intent所在的地方:
image.png
image.png
image.png
可以看到这个intent是存储在ActivityClientRecord里的。
接下来我们就一下看这个intent从哪来的,回溯:
image.png
image.png
不要搞错了,我们要找到的intent是ActivityClientRecord里的intent,而不是这个customIntent。可以看到这个ActivityClientRecord是msg.obj里带过来的:
image.png
如果我们能拿到msg,就可以成功操作intent。所以我们需要拿到这个msg。
这里我们看一下handler:
image.png
这里的mCallback实际上一般都是空的,因为其赋值的地方在它的三参构造方法中,而我们一般使用Handler是用的无参构造方法:
image.png
image.png
所以说,这个mCallback正常情况下是空的,如果不为空的话,在dispatchMessage里就会直接走mCallback.handleMessage的情况,然后return掉,不会调用下面的handleMessage方法。
所以我们的思路就是创建一个Callback对象替换系统的Callback(通过反射替换系统的对象),这样一来,替换的步骤就大致出来了:
替换Callback.msg-->handleMessage(msg)-->msg.obj-->获取ActivityClientRecord对象-->Intent
刚才我们替换的思路就是想要替换intent,那就必须拿到ActivityClientRecord对象,要拿到这个对象,就要拿到msg.obj,为了拿到msg.obj,就要去修改dispatchMessage里的msg,为了修改,就需要创建一个Callback替换掉Handler里空的Callback。
具体步骤:
①新建Handler.Callback,进行intent的替换:
image.png
将代理的intent替换为插件的intent。
②获取对应的Handler,替换调用Handler里的Callback。
image.png
所以我们要替换的就是这里的mH,而这个mH又是ActivityThread里的对象,所以我们再此之前还需要获取ActivityThread对象:
image.png
我们要获取的就是这个sCurrentActivityThread.(有个疑问,为什么不直接反射ActivityThread,直接调用mH呢?)
具体操作:
image.png
这样一来mH这个Handler的mCallback字段就被替换成我们自己实现的Handler.Callabck了。 image.png
当下还有问题,就是所有的startActivity都会走这个,我们应该做一下进程过滤,只有进程不同的时候,才走这块逻辑。

版本适配

6.0和9.0底层实现有所不同,我们需要做一下具体的版本适配:

image.png
可以看到6.0没有ActivityManagerService.getService(),而是
image.png
image.png
ActivityManagerService.getDefault().startActivity...
区别只在于这个api调用的名字不太一样,
主要看一下Android9.0,9.0从AMS出来后,并不会走H类的handleMessage,而是走到了ActivityThread里的EXECUTE_TRANSACTION消息处理中去了:
image.png
时序图:
image.png
从ActivityStackSupervisor的realStartActivityLocked入手:
image.png
image.png image.png
image.png
image.png
image.png

可以看到我们实际要处理的就是这个EXECUTE_TRANSACTION这个事件:

image.png
image.png

9.0适配逻辑如下,不做详情分析了就:


image.png

一般Activity启动流程(不是根Activity)

Activity的启动流程,可以从Context的startActivity说起,其实现是ContextImpl的startActivity。然后内部会通过Instrumentation来尝试启动 Activity,它会调用AMS的startActivity方法,这是一个跨进程过程,当AMS校验完activity的合法性后,会通过ApplicationThread回调到我们的进程中,这也是一个跨进程过程,而applicationThread就是一个binder,回调逻辑是在binder线程池中完成的,所以需要Handler H将其切换到ui线程,第一个消息是LAUNCH_ACTIVITY,它对应handleLaunchActivity,在这个方法里完成了Activity的创建和启动。(8.0)

资源加载插件化

我们分三步进行分析:
①Resources和AssetManager的关系
②资源加载流程源码分析
③手写实现插件的资源加载

Resources和AssetManager的关系
image.png image.png image.png
可以看到,Resources类实际上也是通过AssetManager类来访问那些被编译过的应用程序资源文件的,不过在访问之前,它会先根据资源ID查找得到对应的资源文件名。而AssetManager对象即可以通过文件名访问那些被编译过的,也可以范文没有被编译过的应用程序资源文件。
这样我们就可以通过AssetManager来加载插件中的资源
面试题:raw文件夹和assets文件夹有什么区别?
image.png
raw:Android会自动为这个目录中的所有文件生成一个ID,这一味这很容易就可以访问到这个资源,甚至在xml中都是可以访问的,使用ID访问是最快的。
assets:不会生成ID,只能通过AssetManager访问,xml中不能访问,访问速度会慢些,不过操作更加方便。
getResources方法是context的方法,而Context又分为Activity的Context和Application的Context。
所以我们花开两朵各表一枝:
我们先从Activity的Context去找,查找路径如下:
Activity--Context--Resources--AssetManager
接下来我们需要找到Context和Resources是在哪绑定的,在Activity启动流程中,AMS会走到ActivityThread的handleLaunchActivity中,接着调用performLaunchActivity,在这个方法中,会调用:
createBaseContextForActivity方法创建ContextImpl,也就是Activity的Context:
image.png
image.png
image.png
最终会调用context.setResources方法将Resource绑定到context中,而这里的Resource就是通过resourcesManager.createBaseActivityResource方法创建Resource的。
image.png

这里创建了ResourceKey,传入了resDir参数,这个参数就是资源目录。
这个key会作为参数传入getOrCreateResource方法中去:

image.png
image.png
image.png
image.png
可以看到在ResourceManager的createAssetManager方法中,会调用:
assets.addAssetPath(key.mResDir)方法将资源放到AssetManager里面去。
所以我们Hook点就找到了,就是这个addAssetPath方法,这里加载的宿主的资源。如果我们调用:
assets.addAssetPath(插件的资源),这里加载的就是插件的资源了。

这里就有两个方法:
①创建:不会有资源的冲突,可能会有代码的冲突(再创建一个AssetManager,专门加载插件的资源)
②合并:插件+宿主的资源,可能会有资源的冲突(AAPT处理)
这里我们采用创建方案。

接下来就是撸码来实现资源的插件化了:


image.png

大致思路如上图所示。

上一篇下一篇

猜你喜欢

热点阅读