WWDC20 iOS14 Runtime优化

1. Class结构体变化

iOS 14以前,在磁盘中一个Class大概长这样:缓存

这个类对象包含了最经常使用的信息:指向元类、父类、以及方法的缓存。它还有一个指针指向更多的额外信息class_ro_t,其中 ro表示read only 。这部分信息是只读的,其中包含了类名、方法、协议、实例变量和属性等信息。Swift类和Objective-C类均使用这个结构。安全

当类第一次从磁盘被加载到内存的时候,刚开始就是长这样的。但类一旦被使用,就会产生一些变化。bash

为了理解以后发生了什么,首先咱们须要理解什么是 Clean MemoryDirty Memorymarkdown

  • Clean Memory :被加载后就不会再变化的内存。例如,class_ro_t就是 Clean Memory ,由于它是只读的。
  • Dirty Memory :在进程运行时会发生变化的内存。类结构体一旦被使用就是 Dirty Memory ,由于运行时会写入新的数据,例如它的方法缓存部分。

Dirty MemoryClean Memory 代价更昂贵,由于在进程运行的整个过程当中,都须要被保留; Clean Memory 则能够为其余事情滕出空间,由于当咱们须要时,系统老是能够很容易地从磁盘中从新加载它。数据结构

macOS能够经过内存交换来解决内存不足的问题,但iOS不支持这个技术,因此 Dirty Memory 的代价会更昂贵。 Dirty Memory 就是为何类结构被分为了这两个部分的缘由。固然,若是咱们能够拥有更多的 Clean Memory ,固然是更好的。把不会改变的数据分离出来,咱们就可让大部分的类数据保持为 Clean Memoryapp

一旦类被使用,运行时会分配额外的空间来存储这部分数据,即class_rw_t,其中 rw表示read write 。这个结构体中,咱们只存储运行时产生的数据。ide

  • First SubclassNext Sibling Class 指针让运行时能够遍历当前使用的全部类。
  • MethodsPropertiesProtocols ,这部分也是能够在运行时进行修改的。在实践中发现,其实只有大约10%类的方法会发生变化,因此这部份内存能够获得优化,滕出一些空间。
  • Demangled Name 只会被Swift类所使用,并且除非有须要获取它们的Objective-C名称,甚至都不会用到。

因此后两个不经常使用的部分,咱们又能够拆分出来:函数

这样就把class_rw_t,拆成了2部分。若是确实有须要,咱们才会这部分class_rw_ext_t结构分配内存。大约90%的类都不须要这部分额外的数据,系统就能够节约大概14MB的内存。工具

使用原结构大约须要30MB内存,拆分后能够节约大概14MB。oop

对macOS Big Sur的邮件App进行测试,发现大约有9千多个类使用了class_rw_t结构,而只有大约10%,即9百多个类使用到了class_rw_ext_t结构。

咱们能够简单计算一下,class_rw_t结构大小减半,那么用1.0-(293120-43392)/293120≈14.8\%就是咱们节约的内存。仅仅邮件就节约了大约15%的内存,经过这个优化,整个系统会减小大量 Dirty Memory

若是原来的代码直接访问class_rw_t结构,因为结构内存布局发生了变化,可能产生崩溃。苹果推荐使用运行时API,这样底层的细节会由他们处理。

2. 相关方法列表变化

每一个类都有一个方法列表。当你写了一个方法,这个方法就会加入到方法列表中。运行时会用这些列表来解析发送给对象的消息。

每一个方法包含3个部分的信息。

  • 名称,或者选择器,例如init
  • 方法参数类型的编码,例如@16@0:8
  • 方法的IMP,Objective-C方法最终会编译为一个C函数。

这些信息都是指针,在64位的系统上会占用24字节。

咱们的方法列表是存在于镜像中的,而镜像的加载位置可能在内存的任何地方,这取决于动态连接器的选择。也就是说,连接器须要解析镜像中的指针,修复它们指向内存真实的的位置。这部分会产生额外的消耗。

又因为镜像中的方法都是固定的,不会跑到其余镜像中去。其实咱们不须要64位寻址的指针,只须要32位便可。

这样作有几个好处:

  • 这个偏移量相对镜像是固定的,与镜像加载的位置无关,当它们从磁盘加载进来后就不要进行修复了。
  • 由于再也不须要进行修复了,这部分数据就能够保存在只读内存(Clean Memory )中,这样也更安全。
  • 在64位系统中,指针大小从64位的24字节降低到32位的12字节。根据实际测量,方法列表占用内存大约为80MB,减半的话就能够节约40MB内存。

咱们但愿保持这部分数据是只读的,但若是咱们使用了 Method Swizzling 呢?

苹果会在一个全局表中映射交换的实现。因为交换并非很是常见的操做,因此这个全局表也不会特别大。

此外,在之前的实现中,进行方法交换会致使整个分页Page变成 Dirty Memory 。即仅仅一个交换,就可能形成数千字节的 Dirty Memory ,这是很不划算的。

若是咱们的代码中直接处理了这些底层细节,但没有处理好的话,可能会形成1个64位的指针去读取2个32位的指针值。这是没有意义的,会形成崩溃。一样,苹果推荐使用运行时API,这样底层的细节会由他们处理。

3. 标记指针结构变化

首先,什么是标记指针 Tagged Pointer

这个指针中,其实只使用了中间高亮部分来表示一个真实的对象指针。

因为字节对齐的缘由,低位老是0;因为咱们不会真正用到全部64进行寻址,因此高位也有一部分老是0。

  • Intel处理器

    低位为0表示真实的指针,1表示标记指针。

    前面的3个比特是tag号,表示其类型。例如3表示NSNumber,6表示NSDate

    tag号为7时表示一种扩展的tag,会使用额外的8比特表示类型,但有意义的数据长度更短,例如UIColorNSIndexSet

    通常状况下,只有苹果能够添加标记指针的类型。 但若是你是Swift开发者,则能够建立本身的标记指针。若是你曾用过有类实例对象关联值的枚举,那就像是一个标记指针。

  • ARM64

    • iOS14如下系统

      ARM64中整个反过来了,首位为1表示标记指针,后面3位表示tag号。

      这个高低位的翻转主要是由于objc_msgSend的一个小优化。苹果须要尽量快地处理objc_msgSend的指针,一般是普通指针,标记指针和nil更少见一些。使用一个比较就能够直接肯定是标记真正或者是nil,更容易进入常见的逻辑中。

      #define likely(x) __builtin_expect(!!(x), 1)
      #define unlikely(x) __builtin_expect(!!(x), 0)
      复制代码

      一样,tag号为7时表示一种扩展的tag,会使用额外的8比特表示类型。

    • iOS14

      iOS 14中tag号被移动到了低位。对于现有的工具,例如动态连接器,对于指针的高8位,ARM的特性 Top Biyte Ignore 会直接被忽略。苹果把扩展部分放在了 Top Biyte Ignore 生效的部分。对于字节对齐的指针,低3位老是0,恰好放下3位的tag号。最终,带来的一个有趣的效果就是,一个标记指针的payload中就能够放下一个普通指针了。这就让一个标记指针能够指向一个常量,例如字符串或者其余可能占用 Dirty Memory 的数据结构。

      若是项目中有涉及到这部分的代码,再将来可能产生崩溃。一样,苹果推荐使用运行时API,这样底层的细节会由他们处理。

4. 总结

iOS14以后苹果为咱们带来了3项运行时优化:

  • 更小的类数据结构。
  • 更小的方法列表。
  • 标记指针的变化。

苹果推荐使用运行时API,这样底层的细节会由他们处理。

5. 参考


若是以为本文对你有所帮助,给我点个赞吧~

相关文章
相关标签/搜索