乐趣区

关于android:原理介绍-Apply-Changes-背后的秘密

简介

在 Android 11 上,Android 运行时 (ART) 引入了一个名为 Structural Class Redefinition (类的结构性重定义) 的 JVMTI API 扩大。本文将介绍类的结构性重定义的性能,并介绍在实现该性能所遇到的问题,蕴含咱们对问题的思考、衡量及解决方案。类的结构性重定义是一个运行时性能,它扩大了 Android 8 中引入的重定义类办法,Android Studio 里的 Apply Changes 能够通过它来扭转类的本身构造,并能够在类中减少变量或者办法。

这能够被用在很多弱小的性能中,例如扩大 Apply Changes 来反对向利用中减少新的资源。您能够 查看相干文档 理解 Android Studio‘Apply Changes’性能的工作机制,以及在后续博客中理解其如何应用类的结构性重定义进行扩大。将来 Android Studio 会减少更加综合和功能强大的工具来适配这些新的个性。

JVMTI 是一个规范的 API,开发工具能够通过它在底层与运行时环境进行交互和管制。利用该性能实现了很多咱们熟知的开发工具,从 Android Studio 中的 Network 及 Memory 分析器,到调试器中的模仿框架,如 dexmaker-mockito-inline、MockK,再到 Layout 以及 Database 查看器。您能够在 Android 文档 中找到更多对于 Android JVMTI 的实现以及如何将其利用于您本人的工具中。

结构化重定义

类的结构性重定义基于 Android Oreo (8.0) 中减少的重定义类进行改良。在 Oreo 中,仅有类中已有的办法能力被批改。类中定义的对象布局以及字段集、办法集不能以任何形式进行批改。

类的结构性重定义对类的批改提供了更高的自由度,使已有类中增加全字段和办法成为了可能,对可能新增的字段及办法的类型没有任何限度。新增的字段初始值为 0 或 null,然而如果须要,JVMTI 代理能够应用 JVMTI 提供的其它办法为其初始化。和规范的类重定义一样,以后执行的办法将延用之前的定义,接下来的调用才会应用新定义。为了保障构造类重定义具备清晰统一的语义,如下批改将无奈被执行:

  • 字段和办法被删除或者批改其属性
  • 类名被批改
  • 类的继承关系 (父类及实现的接口) 被批改

联合 Android Studio 的反对当前,类的结构性重定义可用于针对大多数编辑场景来实现 Apply Changes 性能。本文残余局部将介绍咱们是如何实现该性能,以及实现该新的运行时性能须要进行的思考和衡量。

重中之重,性能有害

实现结构化重定义的次要挑战是不能让利用在公布模式下受影响。对于每个开发者来说,当他们的代码在调试模式下运行并且应用相似 Apply Changes 或者调试器这样的工具时,另一侧可能有数百万用户在他们的手机上运行这些利用。因而,一个首要的准则就是任何 ART 中新增的针对开发者的新个性都不能够在利用处于非调试模式的时候影响运行时性能。这意味着咱们不能对运行时外部外围性能进行重大更改。例如咱们不能批改对象的根本布局、内存申请、垃圾回收机制,不能改变类的加载和连贯,以及 dex 字节码的执行。

蕴含 java.lang.Class 对象 (在 ART 中持有本身类型的动态字段) 在内所有对象,在加载之后就曾经确定了其大小和布局。这样的个性使程序得以高效运行,如上图所示的 Parrot 类,咱们可知任何一个 Parrot 对象都领有 piningFor 字段,并保留在偏移量为 0x8 的地位。这意味着 ART 能够生成高效的代码,但与此同时,咱们也无奈在对象被创立之后批改对象的布局,因为减少新字段咱们不仅仅批改了以后类的布局,同时影响了其所有子类。为了实现该性能,咱们须要在无感且保障原子性的状况下,将原来的对象及实例替换成重定义的对应类。

咱们须要深刻运行时外部,能力在不影响性能的前提下实现类的结构性重定义。从根本上讲,对一个类进行结构化重定义有 4 个关键步骤:

  1. 应用新的类定义为每一个被批改的类型创立 java.lang.Class 的对象;
  2. 应用新定义的类型从新创立所有原有类型对象;
  3. 将所有原有对象替换 / 更新成与之对应的新对象;
  4. 确保所有编译后的代码及运行时状态绝对于新类型布局而言都是正确的。

谋求性能

和很多程序一样,ART 本身也是多线程的,一是因为所运行的 DEX 字节码自身带有的多线程个性 (潜在起因),二是为了防止程序在运行时呈现暂停。在任何时刻,ART 都可能同步执行许多操作,如: 执行 Java 语言代码,执行垃圾回收,加载类、调配对象,执行 finalizer 或其它事件。

这意味着单纯地执行重定义行为是存在显著竞争的。举个例子: 如果在咱们从新创立了所有旧对象后,一个新的实例被创立怎么办?因而,咱们必须十分审慎地执行每一个步骤,以确保不会遇到或者创立不统一的状态。咱们须要保障每一个线程都可能理解到上图所示的是原子性的转换过程,并且所有操作是同步实现的。

对此,间接的解决方案为: 当咱们开始执行重定义时,进行所有操作。而后咱们按上述形式执行从新定义 (创立新的类和对象,而后替换旧的对象)。这样带来的益处是,咱们无需付出任何理论投入就能够取得所需的原子性。当发现不统一时,所有的代码都会暂停,因而不统一的状态不会显露出来。惋惜的是,这种办法有几个问题。

其一,这会大大降低处理速度。可能须要从新创立大量的对象,从新加载大量的类 (例如,如果须要编辑 java.util.ArrayList 类,可能有数千个实例与之相干)。更重大的问题是,在所有线程都进行的状况下,调配对象是不可能的,这是为了避免死锁,例如,咱们在分配内存之前去期待一个曾经暂停的 GC 线程先实现回收工作。这种限度深刻到 ART 及其 GC 的设计中。简略地删除此限度来批改它是不可行的,尤其是为了一个仅在调试中应用的个性。又因为结构化重定义的次要操作是重新分配所有重定义的对象,所以去掉限度显然是不可承受的。

那么咱们当初该怎么办呢?就 Java 代码而言,咱们仍须要确保任何的扭转须要立即实现,然而咱们无奈让所有的操作都进行。这里咱们能够利用 Java 语言的个性,线程无奈间接取得堆以及要害的类加载状态,并且重要的 GC 治理线程永远不会调配或加载类。这意味着,咱们暂停运行时其它操作的惟一步骤是替换过程。咱们能够在其余代码仍在运行的状况下调配所有的类及新对象,因为这些线程没有任何新对象的援用,并且这些代码仍是原始代码,所以不会裸露不统一的状态。

如果您对具体实现感兴趣,能够拜访相干链接。Android 开源我的项目 (AOSP) 代码搜寻工具正式公布 这篇文章能够摸索 Android 及 AOSP 是如何创立的。

因为咱们容许利用代码持续运行,因而须要留神的是全副的状态不会因为咱们的操作而扭转。为此,咱们必须按程序认真敞开运行时的每个局部,以确保咱们能够收集所需的所有信息,并且在运行期间该信息不会生效。为了达到咱们的目标,在重定义的时候,咱们须要一个残缺的列表蕴含所有重定义¹的类及其子类的 java.lang.Class 对象,须要一个对应的重定义的类的 Class 对象列表,须要一个蕴含该类全副实例的残缺列表和一个蕴含全部重定义对象的残缺列表。

因为加载新类的状况非常少 (并且咱们须要新的 Class 对象以调配重定义的实例),咱们能够先开始收集被重定义类的列表,并为重定义的类型创立新的 Class 对象。为确保这个列表残缺且无效,咱们须要在创立这个列表前 齐全进行类加载²。为此,咱们须要 从一开始就进行新类的加载,同时需期待正在进行的类定义实现。一旦实现,咱们就能够平安地 收集 和 从新创立 所有重定义类的 Class 对象。

至此,咱们收集了所有所需的类,这些类会被用来从新创立那些须要进行替换的实例。与解决类类似,咱们须要暂停调配对象并期待所有线程 确认,以确保咱们的对象列表是最新的³。在此与解决类类似,咱们 收集所有旧的实例 并对每个实例 创立新版本。

至此咱们领有了所有的新对象,残余要做的就是从旧对象复制字段值并且真正替换到新对象中。因为一旦咱们开始将新对象提供给线程或对象援用,它们将不再处于不可见状态,并且线程在运行时能够任意更改任何字段,咱们须要在执行这最初几个步骤之前 进行所有线程。只有其它所有线程都曾经进行,咱们便能够 将字段值从旧对象复制到新对象。

一旦实现上述操作,咱们就能够 遍历堆 并 应用重定义的新实例替换所有旧实例。当初所残余的就是做一些杂项工作,以确保相干事项可能依据须要失去更新或革除,例如反射对象、各种运行时解析缓存等。咱们还确保可能追踪足够的数据,以容许所有运行的代码在重定义开始时可能继续运行。

总结

有了结构化重定义的性能,许多全新的、更弱小的调试和开发工具就应运而生。咱们曾经探讨过了 Apply Changes 的改良,并且 Android 畛域里许多团队正在钻研基于此性能开发其它弱小的工具。这只是咱们在每个 Android 版本公布时增加的许多改良和新个性中的一部分。欢迎您浏览咱们最近的一篇 文章,对于咱们如何应用 IO prefetching 来改良 Android 11 应用程序的启动工夫。

[1] 在此之前,咱们会执行一些查看,以确保所有的类都合乎重定义条件,并且新的定义都无效,不过这些验证很干燥。

[2] 从技术上来看,持续加载无关的类是平安的,然而因为加载类的工作形式,没有方法尽早辨别这些状况以达到现实成果。

[3] 同样,调配对象与 art 虚拟机跨线程同步机制的交互有很多细节,这些细节使咱们不能单纯地暂停重定义类实例的调配。

退出移动版