Android 编译优化——ART与Dalvik区别

一、Android Runtime (ART) 和 Dalvik

Android Runtime (ART) 是 Android 上的应用和部分系统服务使用的托管式运行时。ART 及其前身 Dalvik 最初是专为 Android 项目打造的。作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。

ART 和 Dalvik 是运行 Dex 字节码的兼容运行时,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。不过,Dalvik 采用的一些技术并不适用于 ART。有关最重要问题的信息,请参阅在 Android Runtime (ART) 上验证应用行为

Dalvik 是一个 Google 开发的 VM(Virtual Machine),用在 Android 手机里的运行系统。 DVM is based on JIT (Just In Time) compilation 这代表说,每一次使用者在 run app 的时候,有部分将被执行的 code 会在那时后被转译成装置懂的语言。当使用者在使用 app 的时候,其他的 code 也会被转译再被储存于内存。因为 JIT 只转译部分的 Code ,所以也不会占太多记忆体空间。

ART 功能

以下是 ART 实现的一些主要功能。

预先 (AOT) 编译

ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。

在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。此实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。但是,一些后处理工具会生成无效文件,Dalvik 可以接受这些文件,但 ART 无法编译这些文件。如需了解详情,请参阅处理垃圾回收问题

垃圾回收方面的优化

垃圾回收 (GC) 会耗费大量资源,这可能有损于应用性能,导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化:

  • 大多采用并发设计,具有一次 GC 暂停
  • 并发复制,可减少后台内存使用和碎片
  • GC 暂停的时间不受堆大小影响
  • 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
  • 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见

开发和调试方面的优化

ART 提供了大量功能来优化应用开发和调试。

支持采样分析器

一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能。

ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。

支持更多调试功能

ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,您可以:

  • 查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程。
  • 询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考。
  • 过滤特定实例的事件(如断点)。
  • 查看方法退出(使用“method-exit”事件)时返回的值。
  • 设置字段观察点,以在访问和/或修改特定字段时暂停程序执行。

 

二、编译方式

Android5.之前,采用的是Dalvik虚拟机,编译方式和Java类似,为JIT,JIT意思是Just In Time Compiler,就是即时编译技术,与Dalvik虚拟机相关。

JIT是干嘛的

JIT在Android2.2到Android4.4版本(7.0版本也有,后文会叙述),JIT的目的是为了提高Android的运行效率。

Dalvik虚拟机可以看做是一个Java虚拟机。在 Android系统初期,每次运行程序的时候,Dalvik负责将dex翻译为机器码交由系统调用。这样有一个缺陷每次执行代码,都需要Dalvik将操作码代码翻译为机器对应的微处理器指令,然后交给底层系统处理,运行效率很低

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

JIT优势:

能够根据当前硬件状况实时编译生成最优机器指令(ps. AOT也能够作到,在用户使用是使用字节码根据机器状况在作一次编译)
能够根据当前程序的运行状况生成最优的机器指令序列
当程序须要支持动态连接时,只能使用JIT
能够根据进程中内存的实际状况调整代码,使内存可以更充分的利用

JIT缺陷

  • 编译须要占用运行时资源,会致使进程卡顿
  • 因为编译时间须要占用运行时间,对于某些代码的编译优化不能彻底支持,须要在程序流畅和编译时间之间作权衡
  • 在编译准备和识别频繁使用的方法须要占用时间,使得初始编译不能达到最高性能
  • 由于JIT Code Cache是内存缓存,因此每次运行都需要重新编译

ART和AOT

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

AOT是干嘛的

JIT是运行时编译,是动态编译,可以对执行次数频繁的dex代码进行编译和优化,减少以后使用时的翻译时间,虽然可以加快Dalvik运行速度,但是有一个很大的问题:将dex翻译为本地机器码也要占用时间。 所以Google在4.4推出了全新的虚拟机运行环境ART(Android RunTime),用来替换Dalvik(4.4上ART和Dalvik共存,用户可以手动选择,5.0 后Dalvik被替换)。

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

AOT的优点

  • 在程序运行前编译,能够避免在运行时的编译性能消耗和内存消耗
  • 能够在程序运行初期就达到最高性能
  • 能够显著的加快程序的启动

AOT的缺陷

  • 应用安装和系统升级之后的应用优化比较耗时(重新编译,把程序代码转换成机器语言)
  • 优化后的文件会占用额外的存储空间(缓存转换结果)

图片来自:dexopt 与 dex2oat 区别

ELF: 包含dex和native code,native code: 经过ART编译后的ELF文件,系统能够直接运行

  • dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。

  • dex2oat 是对 dex 文件的 AOT 提前编译操作,其需要一个 dex 文件,然后对其进行编译,结果是一个本地可执行的 ELF 文件,可以直接被本地处理器执行。

三、Android N混合编译

Android 7.0/7.1的ART引入了全新的Hybrid模式(Interpreter + JIT + 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在安装时不编译, 所以安装速度快。在运行App时, 先走JIT解释器, 然后热点函数会被识别,并被JIT进行编译, 存储在jit code cache, 并产生profile文件(记录热点函数信息)。
等手机进入charging和idle状态下, 系统会每隔一段时间扫描App目录下profile文件,并执行AOT编译(Google官方称之为profile-guided compilation)。不论是jit编译的binary code, 还是AOT编译的binary code, 它们之间的性能差别不大, 因为它们使用同一个optimizing compiler进行编译。
 

优点: App安装速度快,占用存储少(只编译热点函数)。
缺点: 前几次运行会较慢, 只有用户操作得次数越多,jit 和AOT编译后, 性能才会跟上来。

(上图有几个箭头存在问题 )

  • commits code 应该是由optimizing compiler指向jit code cache
  • gets ProfileingInfos 方向是反了
  • jit code cache应该指向ART Runtime:Execute Method

 

Android N的ART模式

JIT的解释器

  • 对字节码进行解释
    • 基于计算的跳转指令
    • 基于Arm汇编的Operation Code处理
  • Profiling以及JIT编译器的触发
    • 基于函数执行次数以及搜索式的代码热度
  • JIT代码缓存
    • 管理编译过的缓存代码
    • 为Hot Methods分配ProfilingInfo对象

JIT的编译器
函数粒度的编译

  • 后台编译
    • 避免Block App的UI线程
  • 基于ART优化的编译器
    • 使用和AOT一样的编译器
  • 在优化编译器中会增强JIT的编译能力

使用单独的ProfileSaver线程生成Profile文件

  • 生成Profile文件
    • 读取根据Hot Methods生成ProfilingInfo
    • 把ProfilingInfo写到磁盘文件中
  • 最低的消耗
    • 减少Wakeup的次数
    • 写入最少的信息

使用混编模式的原因

  • 部分用户只使用APP的部分功能。而且这些经常使用的功能是值得被编译成Native Code的
  • 使用JIT阶段找出来经常使用的代码
  • 使用AOT编译以及优化来提升经常使用的这些功能
  • 避免为了一些不常用的代码而付出资源(编译、存储等等)

混编模式的实现

  • 在JIT的过程中,生成Offline的Profile
  • Offline Profile的文件格式
  • 使用AOT增强过后的编译器(dex2oat)
  • 编译所使用的Daemon Service
    • 只在充电或者系统IDLE的情况下才会进行AOT编译

Profile文件会在JIT运行的过程中生成:

  • 每个APP都会有自己的Profile文件
  • 保存在App本身的Local Storage中
  • Profile会保存所调用的类以及函数的Index
  • 通过profman工具进行分析生成

 

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