虚拟机运行 Android 程序背后的故事

不羁岁月 提交于 2020-08-08 05:32:27

众所周知,Android 最开始面世时,使用的开发语言是 Java,而 Java 是运行在 Java 虚拟机上的,即 JVM。那么为什么 Google 要单独设计一套新的 Dalvik 虚拟机来执行 Android 程序呢?可能是为了解决移动端设备上软件运行效率问题,可能是 JVM 虚拟机无法满足当时移动端的使用场景,也可能是为了规避与 Oracle 公司的版权纠纷问题,最终 Google 专门为 Android 平台设计了一套虚拟机来运行 Android 程序,它就是 Dalvik Virtual Machine(Dalvik 虚拟机)。

随着 Android 发展至今,虽然目前开发 Android 程序的语言已经越来越多样性,如 Java,Kotlin,Dart,Flutter 等等,但无论使用哪种语言开发 Android,最终都需要运行在虚拟机上,本篇文章将站在 Android 虚拟机的视角来分析 Android 程序的运行原理。

1、Dalvik 虚拟机概述及特点

Google 于 2007 年底正式发布了 Android SDK, Dalvik 虚拟机也正式进入我们的视野,而 Dalvik 命名的由来是取至其作者**丹·伯恩斯坦(Dan Bornstein)**曾居住过一个名叫 Dalvik 的小渔村。Dalvik 虚拟机作为 Android 平台的核心组件,拥有如下几个特点:

  • 体积小,占用内存空间小;
  • 专有的 DEX 可执行文件格式,体积更小,执行效率更快;
  • 常量池采用 32 位索引值,寻址类方法名、字段名、常量更快;
  • 基于寄存器架构,并拥有一套完整的的指令系统;
  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能;
  • 所有的 Android 程序都运行在 Android 系统进程里,每个进程对应着一个 Dalvik 虚拟机实例;

2、Dalvik 虚拟机与 Java 虚拟机的区别

从 Dalvik 虚拟机的特点我们可以看出 Dalvik VM 和 JVM 还是有许多的不同点的,两者并不兼容,他们显著的不同点主要有以下几个方面:

2.1、Java 虚拟机运行的是 Java 字节码,Dalvik 虚拟机运行的是 Dalvik 字节码

传统的 Java 程序经过编译,生成 Java 字节码保存在 .class 文件中,Java 虚拟机通过解码 .class 文件中的内容来运行程序。而 Dalvik 虚拟机运行的是 Dalvik 字节码,所有的 Dalvik 字节码由 Java 字节码转换而来,并被打包到一个 DEX(Dalvik Executable) 的执行的文件中,Dalvik 虚拟机通过解释 DEX 文件来执行这些字节码。

2.2、Dalvik 可执行文件的体积更小

Android SDK 中有一个叫 dx 的工具负责将 Java 字节码转换成 Dalvik 字节码。dx 工具对 Java 类文件重新排列,消除在类文件中出现的所有冗余的信息,避免虚拟机在初始化时出现重复的文件加载与解析过程。

一般情况下,Java类文件中包含多个不同的方法签名,如果其他的类文件引用该类文件中的方法,方法签名也会被复制到其类文件中,也就是说,多个不同的类会同时包含相同的方法签名,同样的,大量的字符串常量在多个类文件中也被重复使用,这些冗余信息会直接增加文件的体积,同时也会严重影响虚拟机解析文件的效率。dx 工具针对这个问题专门做了处理,它将所有的 Java 类文件中的常量池进行分解,消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享一个常量池。

由于 dx 工具对常量池的压缩,使得相同的字符串、常量在 DEX 文件中只出现一次,从而减少了文件的体积。其转换过程可参考下图:
在这里插入图片描述

2.3、Dalvik 虚拟机与 Java 虚拟机架构不同

Java 虚拟机基于栈结构。Java 程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分派与内存访问次数,会消耗不少 CPU 执行时间,对与像手机设备资源有限的设备来说,这是相当大的一笔开销了。
Dalvik 虚拟机是基于存储器架构。数据的访问通过多个存储器之间直接传递,这样的访问方式比基于栈的方式要快得多。深入了解请移步 Dalvik 虚拟机和 Sun JVM 在架构和执行方面有什么本质区别

3、Dalvik 虚拟机是如何执行程序的

对 Dalvik 有初步了解之后,要弄明白 Dalvik 虚拟机是如何执行程序的我们可以把这个问题拆分成以下几个问题

  • 1、Dalvik 在整个 Android 系统中所处的角色?
  • 2、Dalvik 是如何被创建且初始化的?
  • 3、Dalvik 的可执行文件 dex 与 apk 之间的关系?
  • 4、Dalvik 是如何加载并执行 dex 文件的?

接下来我们按照这四个问题,来逐步揭开 Dalvik 的面纱。

3.1、Dalvik 在整个 Android 系统中所处的角色?

首先我们通过官网一张图来了解 Dalvik 虚拟机在整个 Android 架构中所处的位置:

在这里插入图片描述
细心的同学乍一看这张图会,发现并没有找到 Dalvik 的位置,这是因为 Android 版本 5.0 之后,Android 逐渐使用 ART 来代替 Dalvik ,他们在整个 Android 系统中充当的角色本质是一样的,没有被改变,它们属于 Android 运行时环境,与一些核心库共同承担着 Android 应用程序的运行工作。

暂时可以理解成 ART 就是 Dalvik 的升级版。那么 ART 又是什么?先等等,后面会讲到

3.2、Dalvik 是如何被创建且初始化的?

从上图我们可以看到 Android 从下往上层主要分为 4 层,如同网络的七层协议,这样做的好处是屏蔽本层与下层的差异。

  • linux内核层(Linux Kernel)
  • 系统运行时库层 (Libraries and Android Runtime)
  • 应用程序框架层(Application Framework)
  • 应用程序层 (Applications)

Android 系统启动加载完内核后,第一个执行的是 init 进程, init 进程首先要做的是设备的初始化工作,然后读取inic.rc 文件并启动系统中的重要外部程序 Zygote。Zygote 进程是 Android 所有进程的孵化器进程,它启动后会首先初始化 Dalvik 虚拟机,然后启动 system_service 进程并进入 Zygote模式,此时通过 socket 等候命令。 当执行一个 Android 应用程序时,system_service 进程通过 socket 通信方式发送命令给 Zygote,Zygote 接收到命令后会立即 fork 自身创建一个 Dalvik 虚拟机实例来执行应用程序的入口函数,这样一个程序的启动就完成了。 具体流程如下:
在这里插入图片描述
当进程 fork 成功之后,执行的工作就交给了 Dalvik 虚拟机。

到这里我们需要接着思考一个问题,我们平时在开发应用程序的过程中视乎没有直接接触 dex, 而是把应用程序代码直接打包成了apk,既然 Dalvik 虚拟机是执行的 dex 文件,那么打包成 apk 之后的包和 dex 文件又是怎样的关系呢?Dalvik 虚拟机又是如何从 apk 中获取到 dex 文件的呢?

3.3、Dalvik 的可执行文件 dex 与 apk 之间的关系?

要解决这个疑惑,首先我们先来看一看,应用程序编译打包成 apk 的过程是怎样的,下面有一张经典的图可以帮助我们切入这个问题:
在这里插入图片描述
从 apk 打包流程可以看出,整个 apk 打包过程简单可以分为 7 个步骤:

  • 1、打包资源文件,生成 R.java 文件;
  • 2、处理 aidl 文件,生成相应的 Java 文件;
  • 3、编译工程源代码,生成相应的 .class 文件;
  • 4、转换所有的 .class 文件,生成 classes.dex 文件;
  • 5、打包生成 Apk 文件;
  • 6、对 Apk 文件进行签名;
  • 7、对签名后的 Apk 文件进行对齐处理;

其中每一个步骤转换都由对应的工具完成,具体如下:

工具名称 功能介绍 在操作系统中的路径
aapt Android资源打包工具 ${ANDROID_SDK_HOME}/platform-tools/appt
aidl Android接口描述语言转化为.java文件的工具 ${ANDROID_SDK_HOME}/platform-tools/aidl
javac Java Compiler java代码转class文件 ${JDK_HOME}/javac或/usr/bin/javac
dex 转化.class文件为Davik VM能识别的.dex文件 ${ANDROID_SDK_HOME}/platform-tools/dx
apkbuilder 生成apk包 ${ANDROID_SDK_HOME}/tools/opkbuilder
jarsigner .jar文件的签名工具 ${JDK_HOME}/jarsigner或/usr/bin/jarsigner
zipalign 字节码对齐工具 ${ANDROID_SDK_HOME}/tools/zipalign

对 Apk 打包详细步骤感兴趣的同学可以移步apk打包详细流程

回到本文,那么 apk 和 dex 的关系从打包过程中就可以发现,是一个包含关系, apk 中包含着 classes.dex,AndroidManifest.xml,resources.arsc 等,这里解压一个实际项目,包含内容如下图:

在这里插入图片描述
ok,搞明白这个关系,我们就清楚了最终打包会生成 apk 文件,此文件包含 Dalvik 虚拟机需要的可执行文件 classes.dex 。但这个过程中还有一个步骤,我们不要被忽略,就是 apk 被安装到手机的这个过程,Android 发展至今,Google 团队从未放弃过对这个过程进行优化,以此来提升 Android 程序的运行效率。那么安装过程发生了什么,我们一起来看看:
Android 系统接收到请求需要安装 apk 程序时,会启动 PackageInstallerActivity ,并接收通过 Intent 传递过来的 apk 文件信息。

PackageInstaller 的源码位于 Android 系统源码的 packages\PackageInstaller 目录。当 PackageInstallerActivity 启动时,会首先初始化一个 PackageManager 与 Package-Parser.Package 对象,接着调用 PackageUtil 类的静态方法 getPackageInfo() 解析程序包信息,如果这一步解析出错,程序就会失败返回,如果成功就会调用 setContentView() 设置 PackageInstallerActivity 的显示视图,接着调用 PackageUtil 类的静态方法 getAppSnippet() 与 initSnippetForNewApp() 来设置 PackageInstallerActivity 的控件显示程序的名称与图标,最后调用 initiateInstall() 方法进行一些其它的初始化工作。

显示界面如下:
在这里插入图片描述
当用户点击了安装按钮时,会构建一个 Installer 对象进行程序的安装工作,其核心代码是通过 socket 向 /system/bin/installd 程序发送 install 指令,installd 程序是开机后常驻与内存中的, 最终调用 install()函数完成对 apk 的安装工作。

Dalvik 是如何加载并执行 dex 文件的?

Dalvik 虚拟机首先通过 loadClassFromDex() 函数完成类的加载工作,每个类被成功解析后都拥有一个 ClassObject 类型的数据结构存储在运行时环境中,虚拟机使用 gDvm.loadedClasses 全局哈希表来存储与查询所有装载进来的类,随后,字节码验证器使用 dvmVerifyCodeFlow() 函数对装载进来的代码进行校验,防止 apk 被恶意篡改,接着虚拟机调用 FindClass() 函数查找并装载 main() 方法类,最后调用 dvmInterpret() 函数初始化解释器并执行字节码流,一个程序的执行过程如下:

在这里插入图片描述

4、关于JIT 与 AOT,以及 ART 的问世

通过观察上面虚拟机执行程序过程我们发现虚拟机只查找并装载 main() 方法类,如果我们对编译过程不做优化的话,每次执行代码,都需要 Dalvik 将 dex 代码翻译为微处理器指令,然后交给底层系统处理,这样效率显然不高,在这种背景下, JIT 即时编译技术问世。

4.1 关于 Dalvik 虚拟机 JIT (即时编译)

JIT(Just-in-time Compilation,即时编译),又称动态编译,是一种通过在运行时动态的将字节码翻译为机器码的技术,使得程序的执行速度更快。

Android 在 2.2 版本中添加了 JIT 编译器,当 App 运行时,每当遇到一个新类,JIT 编译器就会对这个类进行即时编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即 native code),这样在下次执行到相同逻辑的时候,速度就会更快。JIT 编译器可以对执行次数频繁的 dex/odex 代码进行编译与优化,将 dex/odex 中的 Dalvik Code(Smali 指令集)翻译成相当精简的 Native Code 去执行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。

但 JIT 编译有也有明显的缺点:

  • 每次启动应用都需要重新编译(没有缓存)
  • 运行时比较耗电,耗电量大

4.2 关于 ART 虚拟机 AOT(提前编译)

回到文章开头 -》虚拟机在Android 系统中扮演的角色时,仿佛再这里又一次看到 ART 的身影,下面我们就来揭开它的面纱,首先先来看看什么是 AOT:

AOT 是指 “Ahead Of Time”,与 “Just In Time” 不同,从字面来看是说提前编译。

考虑到 JIT 的缺点,所以 Google 在 4.4 推出了全新的虚拟机运行环境 ART(Android RunTime),用来替换Dalvik(4.4上 ART 和 Dalvik 共存,用户可以手动选择,5.0 后 Dalvik 被替换)

AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex 预编译成 ELF 文件,每次运行程序的时候不用重新编译。 同时 ART 对 Garbage Collection(GC)过程的也进行了改进,其优点如下:

  • 只有一次 GC 暂停(Dalvik 需要两次)
  • 在 GC 保持暂停状态期间并行处理
  • 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
  • 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
  • 压缩 GC 以减少后台内存使用和碎片

但是,但是,但是 AOT 也不是现在的终极目标,它也有缺点:

  • 应用安装和系统升级时时间较长(需要预编译)
  • 优化后的文件会占用额外的存储空间(测试发现会增加 10 - 20% 左右)

JIT 和 AOT 共存

Android 7.0上,JIT 编译器被再次使用,采用 AOT/JIT 混合编译的策略,特点是:

  • 应用在安装的时候 dex 不会再被编译
  • App 运行时, dex 文件先通过解析器被直接执行,热点函数会被识别并被 JIT 编译后存储在 jit code cache 中并生成 profile 文件以记录热点函数的信息。
  • 手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。

优点也就是结合了 JIT 与 AOT 的优点:

  • 即使是大应用,安装时间也能缩短到几秒
  • 系统升级能更快地安装,因为不再需要优化这一步
  • 应用的内存占用更小,有些情况下可以降低50%
  • 改善了性能
  • 更低的电池消耗
  • 更高效的 GC 回收

拓展

虽然目前 Android 程序的运行流畅度越来越可观,但明显还没有达到用户满意的程度,作为开发者,我们的任务任重而道远,了解本节内容,掌握 Android 程序运行背后的内容,有助于我们开发出更高效应用以及突破新技术的瓶颈,例如插件化与热更新的原理发生在虚拟机通过 Classloder 装载 dex 的过程中,利用 JIT 及时编译写出高质量的重复使用代码等等。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!