写点什么

理解 Android 虚拟机体系结构,2021Android 通用流行框架大全

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:3165 字

    阅读完需:约 10 分钟

下面是 Dalvik 虚拟机的结构图:


![5 图.png](https://upl


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


oad-images.jianshu.io/upload_images/14140248-e39fb0c18e735d8a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


一个应用首先经过 DX 工具将 class 文件转换成 Dalvik 虚拟机可以执行的 dex 文件,然后由类加载器加载原生类和 Java 类,接着由解释器根据指令集对 Dalvik 字节码进行解释、执行。最后,根据 dvm_arch 参数选择编译的目标机体系结构。

4.1 dex 文件结构

dex 文件结构和 class 文件结构差异的地方很多,但从携带的信息上看,dex 和 class 文件是一致的。



  • header:存储了各个数据类型的起始地址、偏移量等信息。

  • proto_ids:描述函数原型信息,包括返回值,参数信息。比如“test:()V”

  • methods_ids:函数信息,包括所属类及对应的 proto 信息。


优化主要针对以下几个方面:


  • 调整所有字段的字节序和对齐结构中的每一个域

  • 验证 dex 文件中的所有类

  • 对一些特定的类进行优化,对方法里的操作码进行优化


dex 文件经过优化后文件大小会膨胀,大约增加到原来的 1~4 倍。对于内置应用,一般在系统编译后,便会生成优化文件(odex: Optimized dex)。一个 Android 应用程序,需要经过以下过程才可以在 Dalvik 虚拟机上运行:


  • 把 Java 源文件编译成 class 文件

  • 使用 DX 工具把 class 文件转换成 dex 文件

  • 使用 aapt 工具把 dex 文件、资源文件以及 AndroidManifest.xml 文件(二进制格式)组合成 APK

  • 将 APK 安装到 Android 设备运行


4.2 Dalvik 类加载器

一个 dex 文件需要类加载器加载原生类和 Java 类,然后通过解释器根据指令集对 Dalvik 字节码进行解释和执行。Dalvik 类加载器使用 mmap 函数,将 dex 文件映射到内存中,通过普通的内存读取操作即可访问 dex 文件,然后解析 dex 文件内容并加载其中的类到哈希表中。

4.2.1 解析 dex

总的来说,dex 文件可以抽象为三个部分:头部、索引、数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将 dex 文件映射到内存后,Dalvik 会调用 dexFileParse 函数对其进行分析,分析的结果放到 DexFile 数据结构中。DexFile 中的 baseAddr 指向映射区的起始位置,pClassDefs 指向 class 索引的起始位置。为了加快 class 的查找速度,还创建一个哈希表,对 class 名字进行哈希并生成索引。

4.2.2 加载 class

解析工作完成后就进行 class 的加载,加载的类需要用 ClassObject 数据结构来存储。


typedef struct Object {ClassObject* clazz; // 类型对象 Lock lock; // 锁对象} Object;


其中 clazz 指向 ClassObject 对象,还包含一个 Lock 对象。如果其它线程想要获取它的锁,只有等这个线程释放。Dalvik 每加载一个 class 都会对应一个 ClassObject 对象,加载过程会在内存中分配几个区域,分别存放 directMethod, virtualMethod, sfield, ifield。这些信息从 dex 文件的数据区中读取。字段 Field 的定义如下:


struct Field {ClassObject* clazz; //所属类型 const char* name; // 变量名称 const char* signature; // 如“Landroid/os/Debug;”u4 accessFlags; // 访问标记


#ifdef PROFILE_FIELD_ACCESSu4 gets;u4 puts;#endif};


待得到 class 索引后,实际的加载由 loadClassFromDex 来完成。首先它会读取 class 的具体数据,分别加载 directMethod, virtualMethod, ifield 和 sfield,然后为 ClassObject 数据结构分配内存,并读取 dex 文件的相关信息。加载完成后,将加载的 class 通过 dvmAddClassToHash 函数放入哈希表,以方便下次查找;最后,通过 dvmLinkClass 查找该类的超类,如果有接口类则加载相应的接口类。

4.3 Dalvik 解释器

对于任何虚拟机来说,解释器无疑是核心的部分,所有的 Java 字节码都经过解释器解释执行。由于 Dalvik 解释器的效率很重要,Android 分别实现了 C 语言版和各种汇编语言版的解释器。解释器通常是循环执行,需要一个入口函数调用处理程序执行第一条指令,而后每条指令执行时引出下一条指令,通过函数指针调用处理程序。

4.4 内存管理

垃圾收集是 Dalvik 虚拟机内存管理的核心。此处只介绍 Dalvik 虚拟机的垃圾收集功能。垃圾收集的性能在很大程度上影响了一个 Java 程序内存使用的效率。Dalvik 虚拟机使用常用的 Mark-Sweep 算法,该算法分 Mark 阶段(标记出活动对象)、Sweep 阶段(回收垃圾内存)和可选的 Compact 阶段(减少堆中的碎片)


垃圾收集的第一步是标记出活动对象,因为没有办法识别那些不可访问的对象,这样所有未被标记的对象就是可以回收的垃圾。当进行垃圾收集时,需要停止 Dalvik 虚拟机的运行(除垃圾收集外),因此垃圾收集又被称作 STW(stop-the-world)。Dalvik 虚拟机在运行过程中要维护一些状态信息,这些信息包括:每个线程所保存的寄存器、Java 类中的静态字段、局部和全局的 JNI 引用,JVM 中的所有函数调用会对应一个相应 C 的栈帧。每一个栈帧里可能包含对对象的引用,比如包含对象引用的局部变量和参数。所有这些引用信息被加入到一个根集合中,然后从根集合开始,递归查找可以从根集合出发访问的对象。因此,Mark 过程又叫做追踪,追踪所有可被访问的对象。


垃圾收集的第二步就是回收内存。在 Mark 阶段通过 markBits 位图可以得到所有可访问的对象集合,而 liveBits 位图表示所有已经分配的对象集合。通过比较 liveBits 位图和 markBits 位图的差异就是所有可回收的对象集合。Sweep 阶段调用 free 来释放这些内存给堆。


在底层内存实现上,Android 系统使用的是 msspace,这是一个轻量级的 malloc 实现。除了创建和初始化用于存储普通 Java 对象的内存堆,Android 还创建三个额外的内存堆:


  • "livebits"(用来存放堆上内存被占用情况的位图索引)

  • "markbits"(在 GC 时用于标注存活对象的位图索引)

  • “markstack”(在 GC 中遍历存活对象引用的标注栈)


虚拟机通过一个名为 gHs 的全局 HeapSource 变量来操控 GC 内存堆,而 HeapSource 里通过 heaps 数组可以管理多个堆(Heap),以满足动态调整 GC 内存堆大小的要求。另外 HeapSource 里还维护一个名为"livebits"的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构"markstack"和"markbits"都是用在垃圾回收阶段。



上图中"livebits"维护堆上已用的内存信息,而"markbits"这个位图索引则指向存活的对象。 A、C、F、G、H 对象需要保留,因此"markbits"分别指向他们(最后的 H 对象尚在标注过程中,因此没有指针指向它)。而"markstack"就是在标注过程中跟踪当前需要处理的对象要用到的标志栈,此时其保存了正在处理的对象 F、G 和 H。

4.5 Dalvik 的启动流程

Dalvik 进程管理是依赖于 linux 的进程体系结构的,如要为应用程序创建一个进程,它会使用 linux 的 fork 机制来复制一个进程。Zygote 是一个虚拟机进程,同时也是一个虚拟机实例的孵化器,它通过 init 进程启动。此处分析 Dalvik 虚拟机启动的相关过程。



AndroidRuntime 类主要做了以下几件事情:


  • 调用 startVM 创建一个 Dalvik 虚拟机,JNI_CreateJavaVM 真正创建并初始化虚拟机实例

  • 调用 startReg 注册 Android 核心类的 JNI 方法

  • 通过 Zygote 进程进入 Java 层


在 JNI 中,dvmCreateJNIEnv 为当前线程创建和初始化一个 JNI 环境,即一个 JNIEnvExt 对象。最后调用 dvmStartup 来初始化前面创建的 Dalvik 虚拟机实例。函数 dvmInitZygote 调用了系统的 setpgid 来设置当前进程,即 Zygote 进程的进程组 ID。这一步完成后,Dalvik 虚拟机的创建和初始化工作就完成了。

5 Android 的启动

  • 启动电源,加载引导程序到 RAM

  • BootLoader 引导

  • Linux Kernel 启动

  • Init 进程创建

  • Init fork 出 Zygote 进程,Zygote 进程创建虚拟机;创建系统服务


最后看一下学习需要的所有知识点的思维导图。在刚刚那份学习笔记里包含了下面知识点所有内容!文章里已经展示了部分!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习笔记绝对是你的秘密武器!



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
理解Android虚拟机体系结构,2021Android通用流行框架大全