WWDC20 10163 - Objective-C Runtime 的改进

2021年11月23日 阅读数:3
这篇文章主要向大家介绍WWDC20 10163 - Objective-C Runtime 的改进,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

知识点问题梳理
node

这里罗列了一些问题用来考察你是否已经掌握了这篇文章,若是没有建议你加入 收藏 再次阅读。web

  • Dirty Memory 与 Clean Memory 要如何区分?此次的优化是如何分离出 Dirty Memory 部分的?
  • class_rw_tclass_ro_t 分别存放了 class 的什么信息?为何分离出部分  class_rw_t 就能起到优化做用?
  • Methods 中包含哪几个部分?对方法的绝对寻址和相对寻址有什么区别?为何将方法查找改为相对寻址可以节省内存空间?
  • 将方法的寻址方式修改后, Method Swizzling 是否受影响?官方解决的方式是怎样的?
  • Tagged Pointer 是什么?为何能够加快访问和操做速度?
  • 为何在 Intel 和 ARM64 架构下要对 Tagged Pointer 区别对待?这么作的目的是什么?

概述

Objective-C 是一门古老的语言,诞生于 1984 年,跟随 Apple 一路浮沉,见证了乔布斯建立了 NeXT,也见证了乔布斯重回 Apple 重创辉煌,它用它特立独行的语法,堆砌了 UIKitAppKitFoundationKit 等一个个基石。缓存

时间来到 2020 年,面对汹涌的 “后浪” Swift,“老前辈” Objective-C 也在发挥着本身的余热。今年,Apple 给 Objective-C Runtime 带来了新的优化,让咱们看看具体有哪些。安全

类数据结构变化

首先咱们先来了解一下二进制类在磁盘中的表示:微信

首先是类对象自己,包含最常访问的信息:指向元类,超类和方法缓存的指针。在类结构之中有指向包含更多数据的结构体 class_ro_t 的指针,拥有类的名称,方法,协议,实例变量等等编译期肯定的信息。其中 ro 表示 Read Only 的意思。数据结构

当类被 Runtime 加载以后,类的结构会发生一些变化,在了解这些变化以前,咱们须要知道 2 个概念:架构

  • Clean Memory:加载后不会发生更改的内存块, class_ro_t属于 Clean Memory,由于它是只读的。
  • Dirty Memory:运行时会进行更改的内存块,类一旦被加载,就会变成 Dirty Memory,例如,咱们能够在 Runtime 给类动态的添加方法。

这里要明确,Dirty MemoryClean Memory 要昂贵得多。由于它须要更多的内存信息,而且只要进程正在运行,就必须保留它。对于咱们来讲,越多的 Clean Memory 显然是更好的,由于它能够节约更多的内存。app

咱们能够经过分离出永不更改的数据部分,将大多数类数据保留为 Clean Memorydom

在介绍优化方法以前,咱们先来看一下,在类加载以后,类的结构会变成如何呢?以下图:编辑器

在类加载到 Runtime 中后,才会分配用于读取/写入数据的结构体 class_rw_t

class_ro_t 是只读的,存放的是编译期间就肯定的字段信息;而 class_rw_t 是在 Runtime 时才建立的,它会先将class_ro_t的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去

这么设计的缘由是由于 Objective-C 是动态语言,你能够在运行时更改它们方法,属性等,而且分类能够在不改变类设计的前提下,将新方法添加到类中。

事实证实,class_rw_t 会占用比 class_ro_t 占用更多的内存。在系统测量了大约 30MB 的内存占用中,发现大约 10% 的类实际会存在动态的更改行为。这些更改行为包括:动态添加方法、使用 Category 方法等。

所以,官方 将动态的部分提取出来,称之为 class_rw_ext_t,修改以后的结构以下:

通过拆分,能够把 90% 的类优化为 Clean Memory,在系统层面节省了大约 14MB 的内存,使内存可用于更有效的用途。

那么如何验证效果呢?

$ head Mail | egrep 'class_rw|COUNT'

可使用上述命令,来查看 class_rw_t 消耗的内存。在上述例子中,咱们查看的是 Mail 应用的使用状况。

相对方法地址优化

在咱们的认知中,每一个类都包含一个方法列表,以便 Runtime 能够查找和消息发送。结构大概以下图所示:

方法包含了 3 部分的内容:

  • Selector:方法名称或选择器。选择器是字符串,可是它们是惟一的
  • 方法类型编码:方法类型编码标识(详情能够查看参考连接)
  • IMP:方法实现的函数指针

在 64 位系统中,它们占用了 24 字节的空间

了解了方法的结构以后,咱们来看下进程中内存的简化视图

这是一个 64 位的地址空间,其中各类块分别表示了栈,堆以及各类库。咱们把焦点放在 AppKit 库中的init方法。

如图所示,图中的 3 个地址分别为方法的 3 个部分的表示的绝对地址。

咱们知道,库的地址取决于动态连接库加载以后的位置,这是由于ASLR(Address Space Layout Randomization)地址空间布局随机化的存在。

而动态连接器须要修正真实的指针地址,这也是一种代价。因为方法地址仍旧在当前二进制地址空间区域内,因此方法列表并不须要使用 64 位的寻址范围空间,它们仅须要根据自身地址以及偏移量就能够找到其余方法位置

所以咱们可使用 32 位相对偏移来代替绝对 64 位地址。优化以后,方法与内存地址的寻址表现以下:

这么作有几个优势:

  • 不管将库加载到内存中的任何位置,偏移量始终是相同的。所以加载后不须要进行修正指针地址;
  • 能够保存在只读存储器中,这会更加的安全;
  • 使用 32 位偏移量在 64 位平台上所需的内存量减小了一半。

相对方法地址会存在另一个问题,在 Method Swizzling 如何处理呢?

众所皆知,Method Swizzling 替换的是 2 个函数指针的指向。函数能够在任意地方实现,但使用了上述的相对寻址优化以后,这样就没法正常工做了。

针对 Method Swizzling 官方使用全局映射表来解决这个问题,在映射表中维护 Swizzles 方法对应的实现函数指针地址。因为 Method Swizzling 的操做并不常见,因此这个表不会变得很大,新的 Method Swizzling 机制以下图。

Tagged Pointer 格式的变化

首先,让咱们先来了解下 Tagged Pointer 是什么?

Tagged Pointer 是一种特殊标记的对象,经过在其最后一个 bit 位设置为特殊标记位,并将数据直接保存在指针自身中。

Tagged Pointer 是一个“伪”对象,可是在 iOS 开发中带来了 3 倍的访问速度提高、100 倍的建立、销毁速度提高的收益。

这里推荐观看「WWDC2013 - Advances in Objective-C」,会有对 Tagged Pointer 较为详细的讲解。

在 64 位系统中查看对象指针时,咱们会看到 16 进制的地址表示,例如 0x00000001003041e0 将其转换为二进制表示以下:

  在 64 位系统中,咱们有 64 位空间能够表示一个对象指针。因为内存对齐,一般没有真正使用到全部这些位。对象必须位于指针大小倍数的地址中,低位和高位均被 0 填充,所以只用到了中间部分的位,出现了大量的内存浪费

因为以上痛点,按照 Tagged Pointer 的思路,能够将低位设置为 1 加以区分。

而且在最低位以后的 3 位,赋予其类型意义。3 位,能够表示 7 种数据类型

OBJC_TAG_NSAtom = 0
OBJC_TAG_1 = 1
OBJC_TAG_NSString = 2
OBJC_TAG_NSNumber = 3
OBJC_TAG_NSIndexPath = 4
OBJC_TAG_NSManagedObjectID = 5
OBJC_TAG_NSDate = 6
OBJC_TAG_7 = 7

在剩余的字段中,记录所包含的数据。在 Intel 的 x86 架构中,咱们 Tagged Pointer 对象的表示以下

 OBJC_TAG_7 类型的 Tagged Pointer 是个例外,它能够将后 8 位做为扩展字段,基于此咱们能够多支持 256 种类型的 Tagged Pointer,如 UIColors 或 NSIndexSets 之类的对象。

在 ARM64 中表现会不太同样:

最高位表明 Tagged Pointer 标识位,次 3 位标识 Tagged Pointer 的类型,接下去的位来表示包含的数据(可能包含扩展类型字段)

那么在 ARM64 中,为何要用最高位表明的 Tagged Pointer 标记,而不是像 Intel 同样使用低位标记?

它实际是对 objc_msgSend 的微小优化。咱们但愿 objc_msgSend 检索的时间尽量快,这个时间开销是出如今 objc_msgSend 查找指针的一种 Corner Case 上,即对比 Tagged Pointer 指针和 nil。当标记在最高位时,能够经过复杂度 的比较直接完成,无形之中节省了一次遍历的时间。

总结

在 2020 年中,Apple 针对 Objective-C 作了三项优化

  • 类数据结构变化:节约了系统更多的内存(在最新的 Runtime 版本中体现,即 macOS 10.5.5 中已存在);
  • 相对方法地址:节约了内存,而且提升了性能(Xcode developmentTarget > 14 时会自动进行处理);
  • Tagged Pointer 格式的变化;提升了 objc_msgSend 性能(iOS 14, MacOS Big Sur, iPadOS 14 上生效)。

你以为此次的优化是否能大幅度的提高性能呢?欢迎一块儿讨论~

「一瓜技术」编辑的技术文章均有知识点问题梳理,也欢迎你们在下方评论区做答,与你们分享本身的思考结果。


本文分享自微信公众号 - 一瓜技术(tech_gua)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。