iOS 14以前,在磁盘中一个Class
大概长这样:缓存
这个类对象包含了最经常使用的信息:指向元类、父类、以及方法的缓存。它还有一个指针指向更多的额外信息class_ro_t
,其中 ro表示read only 。这部分信息是只读的,其中包含了类名、方法、协议、实例变量和属性等信息。Swift类和Objective-C类均使用这个结构。安全
当类第一次从磁盘被加载到内存的时候,刚开始就是长这样的。但类一旦被使用,就会产生一些变化。bash
为了理解以后发生了什么,首先咱们须要理解什么是 Clean Memory 和 Dirty Memory 。markdown
class_ro_t
就是 Clean Memory ,由于它是只读的。Dirty Memory 比 Clean Memory 代价更昂贵,由于在进程运行的整个过程当中,都须要被保留; Clean Memory 则能够为其余事情滕出空间,由于当咱们须要时,系统老是能够很容易地从磁盘中从新加载它。数据结构
macOS能够经过内存交换来解决内存不足的问题,但iOS不支持这个技术,因此 Dirty Memory 的代价会更昂贵。 Dirty Memory 就是为何类结构被分为了这两个部分的缘由。固然,若是咱们能够拥有更多的 Clean Memory ,固然是更好的。把不会改变的数据分离出来,咱们就可让大部分的类数据保持为 Clean Memory 。app
一旦类被使用,运行时会分配额外的空间来存储这部分数据,即class_rw_t
,其中 rw表示read write 。这个结构体中,咱们只存储运行时产生的数据。ide
因此后两个不经常使用的部分,咱们又能够拆分出来:函数
这样就把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
结构大小减半,那么用就是咱们节约的内存。仅仅邮件就节约了大约15%的内存,经过这个优化,整个系统会减小大量 Dirty Memory 。
若是原来的代码直接访问class_rw_t
结构,因为结构内存布局发生了变化,可能产生崩溃。苹果推荐使用运行时API,这样底层的细节会由他们处理。
每一个类都有一个方法列表。当你写了一个方法,这个方法就会加入到方法列表中。运行时会用这些列表来解析发送给对象的消息。
每一个方法包含3个部分的信息。
init
。@16@0:8
。这些信息都是指针,在64位的系统上会占用24字节。
咱们的方法列表是存在于镜像中的,而镜像的加载位置可能在内存的任何地方,这取决于动态连接器的选择。也就是说,连接器须要解析镜像中的指针,修复它们指向内存真实的的位置。这部分会产生额外的消耗。
又因为镜像中的方法都是固定的,不会跑到其余镜像中去。其实咱们不须要64位寻址的指针,只须要32位便可。
这样作有几个好处:
咱们但愿保持这部分数据是只读的,但若是咱们使用了 Method Swizzling 呢?
苹果会在一个全局表中映射交换的实现。因为交换并非很是常见的操做,因此这个全局表也不会特别大。
此外,在之前的实现中,进行方法交换会致使整个分页Page
变成 Dirty Memory 。即仅仅一个交换,就可能形成数千字节的 Dirty Memory ,这是很不划算的。
若是咱们的代码中直接处理了这些底层细节,但没有处理好的话,可能会形成1个64位的指针去读取2个32位的指针值。这是没有意义的,会形成崩溃。一样,苹果推荐使用运行时API,这样底层的细节会由他们处理。
首先,什么是标记指针 Tagged Pointer ?
这个指针中,其实只使用了中间高亮部分来表示一个真实的对象指针。
因为字节对齐的缘由,低位老是0;因为咱们不会真正用到全部64进行寻址,因此高位也有一部分老是0。
Intel处理器
低位为0表示真实的指针,1表示标记指针。
前面的3个比特是tag号,表示其类型。例如3表示NSNumber
,6表示NSDate
。
tag号为7时表示一种扩展的tag,会使用额外的8比特表示类型,但有意义的数据长度更短,例如UIColor
或NSIndexSet
。
通常状况下,只有苹果能够添加标记指针的类型。 但若是你是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,这样底层的细节会由他们处理。
iOS14以后苹果为咱们带来了3项运行时优化:
苹果推荐使用运行时API,这样底层的细节会由他们处理。
若是以为本文对你有所帮助,给我点个赞吧~