iOS 14 苹果对 Objective-C Runtime 的优化

做者:Damien,iOS 开发者。目前就任于携程。html

Session:developer.apple.com/wwdc20/1016…缓存

概述

Objective-C 是一门古老的语言,诞生于 1984 年,跟随 Apple 一路浮沉,见证了乔布斯建立了 NeXT,也见证了乔布斯重回 Apple 重创辉煌,它用它特立独行的语法,堆砌了 UIKit,AppKit, Foundation 等一个个基石,时间来到 2020 年,面对汹涌的"后浪" Swift,"老前辈" Objective-C 也在发挥着本身的余热,即便面对愈来愈多阵地失守,惟有“老兵不死,只会慢慢凋亡"才能体现的悲壮。今年,Apple 给 Objective-C Runtime 带来了新的优化,接下来,让咱们深刻理解这些变化。安全

类数据结构变化

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

首先是类对象自己,包含最常访问的信息:指向元类,超类和方法缓存的指针,在类结构之中有指向包含更多数据的结构体 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显然是更好的,由于它能够节约更多的内存。咱们能够经过分离出永不更改的数据部分,将大多数类数据保留为Clean Memory,如何怎么作的呢? 在介绍优化方法以前,咱们先来看一下,在类加载以后,类的结构会变成如何呢? app

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

Tips:class_ro_t是只读的,存放的是编译期间就肯定的字段信息;而class_rw_t是在 runtime 时才建立的,它会先将class_ro_t的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去,之因此要这么设计是由于 Objective-C 是动态语言,你能够在运行时更改它们方法,属性等,而且分类能够在不改变类设计的前提下,将新方法添加到类中。dom

事实证实,class_rw_t会占用比class_ro_t占用更多的内存,在 iPhone 中,咱们在系统测量了大约 30MB 的这些class_rw_t结构。应该如何优化这些内存呢?经过测量实际设备上的使用状况,咱们发现大约 10% 的类实际会存在动态的更改行为,如动态添加方法,使用 Category 方法等。所以,咱们能能够把这部分动态的部分提取出来,咱们称之为class_rw_ext_t,因此,结构会变成这个样子。 ide

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

Tips:heap xxxxx | egrep 'class_rw|COUNT’ 你可使用此命令来查看 class_rw_t 消耗的内存。xxxx能够替换为须要测量的 App 名称。如:heap Mail | egrep 'class_rw|COUNT’\'查看 Mail 应用的使用状况。函数

相对方法地址

如今,咱们来看看 Runtime 的第二处的变化,方法地址的优化。 每一个类都包含一个方法列表,以便 Runtime 能够查找和消息发送。结构大概以下图所示: 布局

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

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

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

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

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

如图所示,图中的3个地址分别为方法的 3 个部分的表示的绝对地址,咱们知道,库的地址取决于动态连接库加载以后的位置,ASLR(Address space layout randomization 地址空间布局随机化)的存在,动态连接器须要修正真实的指针地址,这也是一种代价。因为方法实现地址不会脱离当前库的地址范围的特性存在,因此实际上,方法列表并不须要使用 64 位的寻址范围空间。他们只须要可以在本身的库地址中查找引用函数地址便可,这些函数将始终在附近。因此咱们可使用 32 位相对偏移来代替绝对 64 位地址。

如今咱们地址将变成这样

这么作有几个优势:

  1. 不管将库加载到内存中的任何位置,偏移量始终是相同的,所以从加载后不须要进行修正指针地址。
  2. 它们能够保存在只读存储器中,这会更加的安全。
  3. 使用 32 位偏移量在 64 位平台上所需的内存量减小了一半。在 iPhone 中咱们能够节省约 40MB 的内存大小。

优化后,指针所需的内存占用量能够减小一半。

相对方法地址会引起另一个问题,那就是在Method Swizzling如何处理呢?众所皆知,Method Swizzling替换的是 2 个方法函数指针指向,方法函数实现能够在任意地方实现,使用了相对偏移地址了以后,这样就没法工做了。 针对Method Swizzling咱们使用全局映射表来解决这个问题,在映射表中维护Swizzles方法对应的实现函数指针地址。因为Method Swizzling的操做并不常见,因此这个表不会变得很大,新的Method Swizzling机制以下图。

Tagged Pointer 格式的变化

接下来咱们会深刻了解 Tagged Pointer 在 ARM CPU 下的格式变化 首先,让咱们先来了解下 Tagged Pointer 是什么 **Tagged Pointer:**一种特殊标记的对象,Tagged Pointer 经过在其最后一个 bit 位设置为特殊标记位,而且把数据直接保存在指针自己中。Tagged Pointer 是一个"伪"对象,使用 Tagged Pointer 有 3 倍的访问速度提高,100 倍的建立、销毁速度提高。

Tips:Advances in Objective-C

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

在 64 位系统中,咱们有 64 位能够表示一个对象指针,可是咱们一般没有真正使用到全部这些位,因为内存对齐要求的存在,低位始终为0,对象必须始终位于指针大小倍数的地址中。高位也始终为0。实际上咱们只是用中间这一部分的位。
所以,咱们能够把最低位设置为 1,表示这个对象是一个 Tagged Pointer 对象。设置为 0 则表示为正常的对象
在设置为 1 表示为 Tagged Pointer 对象以后,在最低位以后的 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 中,咱们 Tagged Pointer 对象的表示以下

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

上文中,咱们介绍的是在 Intel 中 Tagged Pointer 的表示,在 ARM64 中,咱们状况有些变化。

咱们使用最高位表明 Tagged Pointer 标识位,最低位 3 位标识 Tagged Pointer 的类型,接下去的位来表示包含的数据(可能包含扩展类型字段),为何咱们使用高位指示 ARM上 的 Tagged Pointer,而不是像 Intel 同样使用低位标记?

它实际是对 objc_msgSend 的微小优化。咱们但愿 msgSend 中最经常使用的路径尽量快。最经常使用的路径表示普通对象指针。咱们有两种不常见的状况:Tagged Pointer 指针和 nil。事实证实,当咱们使用最高位时,能够经过一次比较来检查二者。与分别检查 nil 和 Tagged Pointer 指针相比,这会为 msgSend 中的节省了条件分支。

总结

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

  • 类数据结构变化:节约了系统更多的内存。
  • 相对方法地址:节约了内存,而且提升了性能。
  • Tagged Pointer 格式的变化:提升了 msgSend 性能。

经过优化,但愿你们能够享受 iPhone 更好,更快的使用体验。

Tips: 类结构的数据变动会在最新的 Runtime 版本中体现,实测 MacOS 10.5.5 中已经存在。 相对方法地址的优化在 Xcode developmentTarget > 14 时会自动进行处理。 Tagged Pointer 的变化则会在 iOS 14, MacOS Big Sur, iPadOS 14 上生效。

参考连接

TypeEncodeing

Lets build Tagged Pointers

Advances in Objective-C

限时福利

这篇文章的内容来自于 《WWDC20 内参》。在这里给你们推荐一下这个专栏。

「WWDC 内参」系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经作了几年了,口碑一直不错。主要是针对每一年的 WWDC 的内容,作一次精选,并号召一群一线互联网的 iOS 开发者,结合本身的实际开发经验、苹果文档和视频内容作二次创做。

今年一共有 213 个 Session 的内容。《WWDC20 内参》挑选了其中的 135 个 Session,短短两周,已经创做了 83 篇文章。目前正在限时优惠销售,只须要 9.9 元,十分优惠。

看了文章还不过瘾的朋友,抓紧订阅 《WWDC20 内参》 xiaozhuanlan.com/wwdc20 继续阅读把~

关注咱们

咱们开通了公众号「老司机技术周报」,每期发布时公众号(LSJCoiding)会推送消息,欢迎关注。

相关文章
相关标签/搜索