深入了解Tagged Pointer
objc
源码版本:779.1
当然还是推荐使用这个来学习:可编译的源码
在 2013 年苹果推出了首个使用 64 位架构的双核处理器的手机 iphone 5s。为了节约内存以及提高执行效率,苹果使用了一种叫做 ‘Tagged Pointer’ 的技术,现在跟着我来了解一下它吧。
Tagged Pointer
从 5s 开始,iPhone 均使用 arm64 指令集的处理器。在 64 位系统上,一个指针占 8 个字节,而指针指向的实例变量至少需要 16 个字节,并且还需要执行额外的一些操作,例如:申请内存,销毁内存。为了达到优化的目的,苹果将一些存储数据的类,例如 NSString,NSNumber,当它们需要保存的数据不需要占用那么多的字节时,直接将数据保存在“指针”里面。
下面让我们用代码来证实Tagged Pointer
的存在
1 | NSNumber *a = [NSNumber numberWithInt:1]; |
然后打个断点,使用 lldb 的命令调试,x/8xg a
,该命令的意思是从a的起始地址开始,打印 8 个 16进制的 8字节长度的值
输出结果
说明指针 a 并不是指向 NSNumber 实例的指针。
或者下面这样更加直观一点
可以看到 a 并没有 isa 指针,所以它并不是一个 NSNumber 实例指针。
Tagged Pointer 如何存储数据
这里你最好打开源码对照着看。
LSB
在非 arm64 架构中,将最低位即 LSB 设置为 1,与正常的指针进行区分。
这样做的原因是,OC 类在创建实例最终调用的是 C 标准库中的 calloc 函数,它所返回的内存地址是 16 字节对齐的,参考 Aligned memory management?。这样的结果就是指针地址低 4 位都是 0,用最低位来表示也合理的
MSB
在 arm64 架构中,将最高位即 MSB 设置为 1,与正常的指针进行区分。
这样做的原因的是因为在 arm64 架构中,指针只用了低位的 48 位,高 16 位都是空着的,原因可以看下这个 为什么64位机指针只用48个位?
在 objc-internal.h
372 行以及 384 行,我们 我们可以看到掩码 _OBJC_TAG_MASK 的定义:
1 | #if OBJC_MSB_TAGGED_POINTERS |
内置类型和扩展类型
系统启动时生成两个全局的数组 objc_debug_taggedpointer_classes 和 objc_debug_taggedpointer_ext_classes。一个用来存储系统内置的Tagged Pointer
类型,而另一个数组存储扩展的Tagged Pointer
类型。
1 | extern "C" { |
- 内置类型数组的数目为 8,使用高 2 - 高 4 的 3 个 bit 来存储数据
- 扩展类型数组的数目为 256,,使用高 5 - 高 12 的 8 个 bit 来存储数据
是否剩余的 bit 都用来保存数据了呢?答案是否定的,拿 NSUmber 举个例子,它属于内置类型,其 tagged pointer 额外使用 低 1 - 低 4 的 5 个 bit 用来保存数字的类型信息,即使用 56 个 bit 来保存数据。
内置类型如下:
1 | enum |
而当指针的高 1 - 高 4 的 4 个 bit 位均为 1 时,表示这是一个扩展类型。系统自带的扩展类型如下:
1 | enum |
数据混淆
运行下面的代码
1 | - (void)boo |
输出结果:
1 | pointer a is ef59c3d36981ed4b |
等等,不是说 Tagged Pointer
最高 4 位用来保存类型信息,剩下的几位都只用来保存数据嘛,为什么输出结果看起来这么复杂呢?
原因是从 iOS12 开始,为了系统安全,将数据与一个随机数进行 异或(^) 操作进行混淆,混淆函数如下:
1 | static inline void * _Nonnull |
objc_debug_taggedpointer_obfuscator
是一个extern关键字的常量,因为被 extern 声明,所以我们可以用下面的代码来解码,得到真正的值
1 | extern uintptr_t objc_debug_taggedpointer_obfuscator; |
输出结果:
1 | pointer a real value is b000000000000012 |
从输出结果可以看出,这几个值都是 0Xb 开头,16进制的 b 用 二级制表示为 1011,最高1用来表示这是一个 Tagged Pointer
,而剩余 3 位的10进制数为 3,符合之前的定义OBJC_TAG_NSNumber = 3
。
至于为什么结尾都是 0x2,这个后面再解释。
下面让我们测试下 NSNumber 的 Tagged Pointer 使用多少位来保存数据。从之前的探究我们知道内置类型用 60 位来保存数据,而经过上面的实验我们可以看到还有 4 位用来做别的事了,那么是否剩余的 56 位都用来保存数据了呢?
1 | - (void)foo |
输出结果:
1 | pointer d real value is b800000000000013 |
结果符合预期。至于为什么最高位 b 后面的数字是 8,是因为高位 5 的位置变成了 1,用来表示这个数是负数(0则表示正数), 而 7 的二进制表示为 ob111,高位 5-8 连起来就是 0b1111,也就是16进制的 8 了。
还一个值得注意的是低位第一位的数字变成了 3,而不是之前的正整数 2,由此我们可以推测最低位的 4 位是用来表示存储数据类型的数据,例如 int,float,bool 这几个类型生成的 Tagged Pointer
最低4位数字应该是不同的。用下面的代表再来验证下:
1 | NSNumber *a = [NSNumber numberWithInt:1]; |
输出结果
1 | pointer a real value is b000000000000012 |
结果符合预期,说明我们的推测是正确的
参考
NSNumber 与 Tagged Pointer
深入解构 objc_msgSend 函数的实现
希望大家看了有所收获吧。
深入了解Tagged Pointer