ART实际就是Android runtime的缩写,他是Android版本新的虚拟机
1.诞生
ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。
2.区别
1.预先编译
ART模式与Dalvik模式最大的不同在于,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。并且对比dalvik,ART具有更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。此实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。但是,一些后处理工具会生成无效文件,Dalvik 可以接受这些文件,但 ART 无法编译这些文件。
2.垃圾回收(GC)时优化
垃圾回收 (GC) 会耗费大量资源,这可能有损于应用性能,导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化:
1.大多采用并发设计,具有一次 GC 暂停
2.并发复制,可减少后台内存使用和碎片
3.GC 暂停的时间不受堆大小影响
4.在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
5.优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
3.开发和调试方面的优化
ART 提供了大量功能来优化应用开发和调试。
支持采样分析器
一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能。
ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。
支持更多调试功能
ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如可以:
1.查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程。
2.询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考。
3.过滤特定实例的事件(如断点)。
4.查看方法退出(使用“method-exit”事件)时返回的值。
5.设置字段观察点,以在访问和/或修改特定字段时暂停程序执行。
优化了异常和崩溃报告中的诊断详细信息
当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 java.lang.ClassCastException、java.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多异常详细信息。(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息。)
3.一些基本名词
1.dex文件
App所有java源代码编译后生成众多class文件,由DX/D8,编译为一个/多个(multiDex)dex文件,由Android虚拟机编译执行
2.odex文件
dex文件经过验证和优化后的产物,art下的odex文件包含经过AOT编译后的代码以及dex的完整内容,但Android8.0之后odex中的dex内容移动到了.vdex文件
3.art文件
art下根据配置文件生成odex文件时同时生成.art文件,主要是为了提升运行时加载odex中热点代码的速度,包含了类信息和odex中热点方法的索引,运行App时会首先根据这个文件来加载odex中已经编译过的代码;执行过speed-profile命令的app才会生成.art文件
4.JIT编译(Just In Time)
由于解释器方式运行太慢引入,对于频繁运行的热点代码(判定标准一般是在某个时间段内执行次数达到某个阈值)进行实时编译(在ART下以方法为粒度)执行,并且缓存JIT编译后的代码在内存中用于下次执行。由于以方法为粒度(ArtMethod)进行编译,JIT编译较于解释器可以生成效率更高的代码,运行更快
5.AOT编译(Ahead-Of-Time)
应用安装时全量编译所有代码为本地机器码,运行时直接执行机器码。
6.JIT编译与AOT编译对比
JIT编译
优势
1.能够根据当前硬件状况实时编译生成最优机器指令(ps. AOT 也能够作到,在用户使用是使用字节码根据机器状况在作一次编译)
2.能够根据当前程序的运行状况生成最优的机器指令序列
3.当程序须要支持动态连接时,只能使用 JIT
4.能够根据进程中内存的实际状况调整代码,使内存可以更充分的利用
劣势
1.编译必须要占用线程资源,导致卡顿
2.因为编译时间须要占用运行时间,对于某些代码的编译优化不能彻底支持,须要在程序流畅和编译时间之间作权衡
3.在编译准备和识别频繁使用的方法须要占用时间,使得初始编译不能达到最高性能
4.由于 JIT Code Cache 是内存缓存,因此每次运行都需要重新编译
AOT编译
优势
1.在程序运行前编译,能够避免在运行时的编译性能消耗和内存消耗
2.能够在程序运行初期就达到最高性能
3.能够显著的加快程序的启动
劣势
1.应用安装和系统升级之后的应用优化比较耗时(重新编译,把程序代码转换成机器语言)
2.优化后的文件会占用额外的存储空间(缓存转换结果)
4.运作方式
一.4.4~7.0
最开始ART只采用AOT编译,在App安装时就编译所有代码存储在本地,打开App直接运行,这样做的优点是应用运行速度变快,缺点也很明显,App安装时间明显变长,而且占用存储空间较大。
二.7.0
Android N之后对于ART进行改动,重新引入了JIT编译,结合使用AOT/JIT混合编译,主要机制如下:
安装时不进行任何编译,前几次运行仅通过解释器解释运行,同时对热点代码进行JIT编译,并将这些代码的相关信息记录在一个配置文件里(data/misc/profile/cur/0/…)
设备处于空闲和充电状态时,编译守护进程读取配置文件对热点代码进行AOT编译并写入到app对应的odex文件中
再次启动应用后优先使用AOT编译过的代码,否则使用解释器+JIT编译,重复这个过程
对于一些庞大的APP,比如淘宝,有些功能可能你一辈子都不会用到,根据上述策略这部分代码就不会被编译保存,从而减少了存储空间的占用。另外,在系统升级时也避免了全量编译所有现存应用造成的时间空间消耗。
三.8.0
Android 8.0引入了.vdex文件,它里面包含 APK 的未压缩 DEX 代码,以及一些用于加快验证速度的元数据。怎么理解呢?这里我们需要补充一下对dex文件的编译配置和系统ROM中各类应用的默认编译方式:
编译选项(compiler filters):
1.verify:只对 DEX 文件进行代码验证,校验各部分合法性。
2.quicken:在verify的基础上优化一些 DEX 指令,提升解释器性能。
3.speed:在verify的基础对所有方法进行 AOT 编译。
4.speed-profile:在verify的基础对配置文件中列出的方法(热点方法)进行 AOT 编译。
系统ROM中各类应用默认编译方式:
1.启动类路径代码(用于启动系统的部分代码):默认使用 speed 编译过滤器进行编译
2.系统服务器代码(比如电量、多媒体服务代码):默认使用 speed 编译过滤器进行编译
3.产品专属的核心应用(比如goole服务框架):默认使用 speed 编译过滤器进行编译
4.所有第三方应用:默认使用 quicken 编译过滤器进行编译
所有第三方应用最开始都是通过quiken模式只进行了dex校验和一些指令优化,.vdex文件存放的就是经过校验后的dex代码,以便在对热点代码进行AOT编译时避免重复验证,加快速度。
四.混合编译的优势
1.应用安装时间过长;在 N 之前,应用在安装时需要对所有 ClassN.dex 做 AOT 机器码编译,类似微信这种比较大型的 APP 可能会耗时数分钟。但是往往我们只会使用一个应用 20% 的功能,剩下的 80% 我们付出了时间成本,却没带来太大的收益。
2.降低占 ROM 空间;同样全量编译 AOT 机器码,12M 的 dex 编译结果往往可以达到 50M 之多。只编译用户用到或常用的 20% 功能,这对于存储空间不足的设备尤其重要。
3.提升系统与应用性能;减少了全量编译,降低了系统的耗电。在 boot.art 的基础上,每个应用增加了 base.art (这块后面会详细解析), 通过预加载与缓存提升应用性能。
4.快速的系统升级;以往厂商 ota 时,需要对安装的所有应用做全量的 AOT 编译,这耗时非常久。事实上,同样只有 20% 的应用是我们经常使用的,给不常用的应用,不常用的功能付出的这些成本是不值得的。
综合下来,混合编译的模式因为在安装的时候不需要进行编译,安装速度会很快。在以后运行app时,会抓取热点数据进行存储并执行AOT编译优化体验。所以在前几次打开应用时速度会相对较慢,在操作次数多了后性能将会跟上。
过滤器
ART 如何编译 DEX 代码还有个compile filter以参数的形式来决定:从 Android O 开始,有四个官方支持的过滤器:
verify:只运行 DEX 代码验证。
quicken:运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解释器性能。
speed-profile:运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。
speed:运行 DEX 代码验证,并对所有方法进行 AOT 编译。
verify 和quicken 他俩都没执行编译,之后代码执行需要跑解释器。而speed-profile 和 speed 都执行了编译,区别是speed-profile根据profile记录的热点函数来编译,属于部分编译,而speed属于全编。
所以
执行效率上:
verify < quicken < speed-profile < speed
编译速度上:
verify > quicken > speed-profile > speed
五.查看编译模式
在命令行中输入adb getprop | grep pm.dex,可以查看当前手机在不同场景下的默认编译模式
一般用到的主要是这几个
- ab-ota:系统升级,这里是使用A/B系统升级方式,也叫做无缝更新,A/B系统升级,顾名思义是有两个系统,在磁盘上开辟两个存储空间A/B存储空间,在升级过程中保证有一个可以正常运行的系统
- bg-dexopt:后台编译
- boot-after-ota:启动后升级
- cmdline:命令行
- first-boot:首次开机安装,google建议设置这个,避免开机的时间过长
- inactive:不活跃的。这个不活跃状态也就是idle,判定是在frameworks/base/core/res/res/config.xml中有一个节点config_jobSchedulerInactivityIdleThreshold,默认值是1860000毫秒,也就是31分钟。31分钟处于非活动状态就会被认为是idle。
- install:安装,设置为speed-profile可以在后续使用中提高应用启动时间,但是如果没有提供配置文件或者配置文件为空,那么启动时间会与verify没有区别
那么这个设置是在哪里进行配置的呢?
build/make/target/product/runtime_libart.mk
# The install filter is speed-profile in order to enable the use of
# profiles from the dex metadata files. Note that if a profile is not provided
# or if it is empty speed-profile is equivalent to (quicken + empty app image).
# Note that `cmdline` is not strictly needed but it simplifies the management
# of compilation reason in the platform (as we have a unified, single path,
# without exceptions).
PRODUCT_SYSTEM_PROPERTIES += \
pm.dexopt.post-boot?=extract \
pm.dexopt.install?=speed-profile \
pm.dexopt.install-fast?=skip \
pm.dexopt.install-bulk?=speed-profile \
pm.dexopt.install-bulk-secondary?=verify \
pm.dexopt.install-bulk-downgraded?=verify \
pm.dexopt.install-bulk-secondary-downgraded?=extract \
pm.dexopt.bg-dexopt?=speed-profile \
pm.dexopt.ab-ota?=speed-profile \
pm.dexopt.inactive?=verify \
pm.dexopt.cmdline?=verify \
pm.dexopt.shared?=speed
在这个文件里会提供一个没有frameworks的安装环境
User 版本会预先提取有source code的APK的odex文件,因为有source code的APK在 Android.mk中都会通过include $(BUILD_PACKAGE)来编译,会调用到package.mk来提取odex
但User版本不会预先提取通过prebuilt方式预置的APK的odex文件,因为采用prebuilt方式预置 APK是只有APK文件而没有source code时在Android.mk中通过 include $(BUILD_PREBUILT) 预置APK,代码中原本的prebuilt.mk中不会提取odex
user版本默认实现了apk预编译优化.如何实现不生成odex? 以下两种方式皆可
1.通过修改build/make/core/board_config.mk中WITH_DEXPREOPT := false
2.在APK的Android.mk文件中添加 LOCAL_DEX_PREOPT := false 来关闭预优化
Dex2oat 选项
请注意,这些选项在设备编译期间以及预先优化期间都会影响 dex2oat
,但是前面讨论的大多数选项都只会影响预先优化。
在 dex2oat
编译启动映像时对其进行控制:
- dalvik.vm.image-dex2oat-Xms:初始堆大小
- dalvik.vm.image-dex2oat-Xmx:最大堆大小
- dalvik.vm.image-dex2oat-filter:编译过滤器选项
- dalvik.vm.image-dex2oat-threads:要使用的线程数
在 dex2oat
编译除启动映像之外的所有内容时对其进行控制:
- dalvik.vm.dex2oat-Xms:初始堆大小
- dalvik.vm.dex2oat-Xmx:最大堆大小
- dalvik.vm.dex2oat-filter:编译过滤器选项
Android 6.0 之前的版本提供了一个适用于编译除启动映像之外的所有内容的附加选项:
- dalvik.vm.dex2oat-threads:要使用的线程数
从 Android 6.1 开始,该选项变成了两个适用于编译除启动映像之外的所有内容的附加选项:
- dalvik.vm.boot-dex2oat-threads:启动时要使用的线程数
- dalvik.vm.dex2oat-threads:启动后要使用的线程数
Android 7.1 及之后的版本提供了两个选项来控制编译除启动映像之外的所有内容时的内存使用方式:
- dalvik.vm.dex2oat-very-large:停用 AOT 编译的最小总 dex 文件大小(以字节为单位)
- dalvik.vm.dex2oat-swap:使用 dex2oat 交换文件(用于低内存设备)
不应减小用于控制 dex2oat
初始堆大小和最大堆大小的选项数值,因为它们可能会限制可对哪些应用进行编译。
从 Android 11 开始,我们提供了 3 个 CPU 亲和性选项,通过这些选项,编译器线程可以限定在特定的一组 CPU 上:
- dalvik.vm.boot-dex2oat-cpu-set:在启动时运行 dex2oat 线程的 CPU
- dalvik.vm.image-dex2oat-cpu-set:在编译启动映像时运行 dex2oat 的 CPU
- dalvik.vm.dex2oat-cpu-set:在启动后运行 dex2oat 线程的 CPU
单独APP安装模式
搜索app包下device.mk文件查找PRODUCT_DEXPREOPT_SPEED_APPS属性
例如device/google/coral/device.mk文件下就有这样一个属性
# Preopt SystemUI
PRODUCT_DEXPREOPT_SPEED_APPS += SystemUIGoogle # For internal
PRODUCT_DEXPREOPT_SPEED_APPS += SystemUI # For AOSP
单独为systemUI设置了speed编译模式