深入了解Tagged Pointer
objc
源码版本:779.1
当然还是推荐使用这个来学习:可编译的源码
在 2013 年苹果推出了首个使用 64 位架构的双核处理器的手机 iphone 5s。为了节约内存以及提高执行效率,苹果使用了一种叫做 'Tagged Pointer' 的技术,现在跟着我来了解一下它吧。
Tagged Pointer
从 5s 开始,iPhone 均使用 arm64 指令集的处理器。在 64 位系统上,一个指针占 8 个字节,而指针指向的实例变量至少需要 16 个字节,并且还需要执行额外的一些操作,例如:申请内存,销毁内存。为了达到优化的目的,苹果将一些存储数据的类,例如 NSString,NSNumber,当它们需要保存的数据不需要占用那么多的字节时,直接将数据保存在“指针”里面。
下面让我们用代码来证实Tagged Pointer
的存在
NSNumber *a = [NSNumber numberWithInt:1];
然后打个断点,使用 lldb 的命令调试,x/8xg a
,该命令的意思是从a的起始地址开始,打印 8 个 16进制的 8字节长度的值
输出结果
说明指针 a 并不是指向 NSNumber 实例的指针。
或者下面这样更加直观一点
image可以看到 a 并没有 isa 指针,所以它并不是一个 NSNumber 实例指针。
Tagged Pointer 如何存储数据
这里你最好打开源码对照着看。
LSB
在非 arm64 架构中,将最低位即 LSB 设置为 1,与正常的指针进行区分。
这样做的原因是,OC 类在创建实例最终调用的是 C 标准库中的 calloc 函数,它所返回的内存地址会是 16 的倍数,参考 Aligned memory management?,虽然里面回答的是 malloc()
函数。。。但我也只能这么解释了。这样的结果就是指针地址低 4 位都是 0,用最低位来表示也合理的
MSB
在 arm64 架构中,将最高位即 MSB 设置为 1,与正常的指针进行区分。
这样做的原因的是因为在 arm64 架构中,指针只用了低位的 48 位。原因可以看下这个 为什么64位机指针只用48个位?
在 objc-internal.h
372 行以及 384 行,我们 我们可以看到如下的定义来证实:
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
# define _OBJC_TAG_INDEX_SHIFT 60
#else
# define _OBJC_TAG_MASK 1UL
# define _OBJC_TAG_INDEX_SHIFT 1
#endif
在 objc-runtime-new.mm 7752 行,我们可以看到tagged pointer
类型的注册函数
void
_objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls)
{
if (objc_debug_taggedpointer_mask == 0) {
_objc_fatal("tagged pointers are disabled");
}
Class *slot = classSlotForTagIndex(tag);
if (!slot) {
_objc_fatal("tag index %u is invalid", (unsigned int)tag);
}
Class oldCls = *slot;
if (cls && oldCls && cls != oldCls) {
_objc_fatal("tag index %u used for two different classes "
"(was %p %s, now %p %s)", tag,
oldCls, oldCls->nameForLogging(),
cls, cls->nameForLogging());
}
*slot = cls;
// Store a placeholder class in the basic tag slot that is
// reserved for the extended tag space, if it isn't set already.
// Do this lazily when the first extended tag is registered so
// that old debuggers characterize bogus pointers correctly more often.
if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
if (*extSlot == nil) {
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
*extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
}
}
}
从上面的函数我们可以了解到,初始化系统时,会生成两个全局的数组变量,一个用来存储系统内置的Tagged Pointer
类型,而另一个数组用来存储自定义扩展的Tagged Pointer
类型。这两个数组的定义如下(objc-object.h 45行):
extern "C" {
extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT];
extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
存储内置类型的数组大小为 16,存储扩展类型的数组大小为 256。
当属于内置类型时,指针的最高2-4位用来存储类型的索引位置,剩余的 60 位用来存储数据(其实只有 56 位,还有 4 位用来保存数据的类型信息)。内置类型有以下几种:
enum
{
// 60-bit payloads
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,
}
而当指针的最高 4 位均为 1 时,则表示这是一个扩展类型。此时,指针的最高 5-12 位用来存储类型的索引信息,而剩余的 52 位用来存储数据。系统定义好的扩展类型有以下几种
enum
{
// ...
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
// ...
}
让我们用代码来验证下是否正确
- (void)boo
{
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithInt:2];
NSNumber *c = [NSNumber numberWithInt:16];
NSLog(@"pointer a is %lx", a);
NSLog(@"pointer b is %lx", b);
NSLog(@"pointer c is %lx", c);
NSLog(@"pointer d is %lx", d);
}
输出结果:
pointer a is ef59c3d36981ed4b
pointer b is ef59c3d36981ed7b
pointer c is ef59c3d36981ec5b
等等,不是说 Tagged Pointer
最高 4 位用来保存类型信息,剩下的几位都只用来保存数据嘛,为什么输出结果看起来这么复杂呢?
原因是从 iOS12 开始,为了系统安全,对Tagged Pointer
的值进行混淆。混淆函数如下:
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
而objc_debug_taggedpointer_obfuscator
是一个extern关键字的常量,既然被 extern 声明,让我们可以用下面的代码来解码,获取真正的值
extern uintptr_t objc_debug_taggedpointer_obfuscator;
- (void)foo
{
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithInt:2];
NSNumber *c = [NSNumber numberWithInt:16];
NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
}
输出结果:
pointer a real value is b000000000000012
pointer b real value is b000000000000022
pointer c real value is b000000000000102
从输出结果可以看出,这几个值都是 0Xb 开头,16进制的 b 用 二级制表示为 1011,最高1用来表示这是一个 Tagged Pointer
,而剩余 3 位的10进制数为 3,符合之前的定义OBJC_TAG_NSNumber = 3
。
至于为什么结尾都是 0x2,这个后面再解释。
下面让我们测试下 NSNumber 的 Tagged Pointer 使用多少位来保存数据。从之前的探究我们知道内置类型用 60 位来保存数据,而经过上面的实验我们可以看到还有 4 位用来做别的事了,那么是否剩余的 56 位都用来保存数据了呢?
- (void)foo
{
NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];
NSLog(@"pointer a real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));
}
输出结果:
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位数字应该是不同的。用下面的代表再来验证下:
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithShort:2];
NSNumber *c = [NSNumber numberWithFloat:1.];
NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];
NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer d real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));
输出结果
pointer a real value is b000000000000012
pointer b real value is b000000000000021
pointer c real value is b000000000000014
pointer d real value is b800000000000013
结果符合预期,说明我们的推测是正确的
Tagged Pointer 如何使用方法
经过上面的探究,我们知道了Tagged Pointer
只是一个基本数据类型,在栈中分配内存。可我们平时使用时却是拿对象来对待它的,那么,当我们对一个Tagged Pointer
对象使用方法时,runtime 是如何处理的呢?
因为消息用到的函数objc_msgSend
使用汇编编写的,我看不大懂。。。但大致的流程是,根据指针高位存储的信息得到相应类的 isa 指针,然后找到方法的 IMP,然后在方法中执行操作。因为不懂得汇编以及逆向我这里就不讲了。
参考
NSNumber 与 Tagged Pointer
深入解构 objc_msgSend 函数的实现
希望大家看了有所收获吧。