前言
在之前描述isa和objc_object的结构体的时候,都有涉及到TaggedPointer的概念。考虑到TaggedPointer本身也有其自己的一套内存结构和特征,因此,专门拿出来做一个专题。
何为TaggedPointer
TaggedPointer直译的话,就是“带有标记的指针”。实际上TaggedPointer是一种及其特殊的对象。我们都知道在iOS中,多有的对象都是objc_object的机构体。当我们声明一个指针后,指针的地址就指向它。而TaggedPointer不一样,它不能称其为一个指针,但它确实也是64位长。在这64位当中,不仅标记了TaggedPointer到底是什么类型的值。更关键的是,TaggedPointer的值本身也被存在了这个64位长度当中。具体如下图所示:
上图中描述的是iOS设备上的内存布局。如果是其他设备上内存布局会有所变化,但这不在我们的讨论范围。
我们可以看到TaggedPointer中主要由4部分组成。
第一部分只占1位,是nonpointer位,这与isa中的内存布局是一样的,且含义也一样。
第二部分占3个位,其作用是标记当前TaggedPointer的实际类型的编号。
第三部分占56个位,主要用来存储TaggedPointer的值。
第四部分占4个位,用来记录当前值的长度。
这里要注意的是,如果直接在设备上打印地址,即使你看到它是一个TaggedPointer对象,但其地址仍旧不会展现成上图中的内存分布。这是因为系统为TaggedPointer的地址做了混淆。
源码解析
我们从源码当中就可以看出端倪
//objc-internal.h
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
这个函数就是用来生成一个TaggedPointer的方法,其入参是一个Tag类型,和一个64位的值。这里可以先说结论,入参tag就是TaggedPointer的类型索引下标。也就是之前篇幅里提到的从objc_tag_classes数组中获取类型。第二个64位的参数就是TaggedPointer的值,可以认定的是,这64位中,后4位是值的长度,接着的56位都是值的存储空间。
现在回到源码上,根据上面的代码。
- 判断类型的值是否小于等于OBJC_TAG_Last60BitPayload的值,那么我们先看一下这个值的定义。
{
// 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,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 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,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
从这个代码中,我们可以看到OBJC_TAG_Last60BitPayload的值位6,也就是说上面的代码规定了系统定义的标准的TaggedPointer只有7种,也就是最开头的0~6的类型。剩下的都被认定为扩展的TaggedPointer类型。
根据前面的条件语句判断,先来看看如果为true的情况:
声明一个64位的值,然后将tag和value一顿操作,最终获得一个result的值,接着调用_objc_encodeTaggedPointer函数来进行编码。
先来看看那一顿操作都是什么
(1)((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) 将tag的值左移动60位(_OBJC_TAG_INDEX_SHIFT的值为60)。这样就相当于只保留了tag原值的最后4位。根据前面的定义,tag的系统类型值有7种,因此用4位也足以保存了。
(2)((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) 将value的值先左移4位,再右移4位。这就相当于把value值的前4位去掉,再去掉由于左移自动填上的尾部4个0的,最终掐头去尾的值。
(3)用第一步和第二步的值进行或运算,相当于把第一步的值填在了第二步值的前4位上。此时,TaggedPointer的值已经是头4位为类型,后面是value+长度的值。
(4)第三步的值与_OBJC_TAG_MASK做或运算。_OBJC_TAG_MASK的定义是1UL<<63,相当于是1后面跟着63个0。此时第三部的值的第一位就变成了1。如果按照isa来看,这就相当于第一位nonpointer位设置为了1。
(5)将最终的值赋值给result变量,并传入_objc_encodeTaggedPointer函数进行编码,并返回结果。如果判断条件为false的情况:
false的情况就意味着tag的值一定大于6。而从上面的定义看,7为保留字段,因此可以断定扩展的taggedPointer的tag值一定大于等于8。在明确这一点后,仍旧是先声明一个64为的result变量,然后再对tag和value进行操作
(1)((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) 。先将tag减去8(OBJC_TAG_First52BitPayload = 8),然后再左移动52位,也就留下了底12位的值。
(2)(value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT)。将value的值去掉头12位。
(3)第一步和第二步进行合并,将第一步的头12位写到底二步的值的里面。
(4)与_OBJC_TAG_EXT_MASK(oxff)做或运算,即,将最终值的头4位全部变成1。
(5)将最终的值赋值给result变量,并传入_objc_encodeTaggedPointer函数进行编码,并返回结果。到这里来看看_objc_encodeTaggedPointer都做了什么事情。
extern uintptr_t objc_debug_taggedpointer_obfuscator;
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
从上面的代码上就可以看出,所谓的编码就是用传进来的值,也就是前面说的result与一个objc_debug_taggedpointer_obfuscator的值进行异或。这样做完,你看到的TaggedPointer的值就更像一个指针的地址而不是结构明显的值了。顺便说一下objc_debug_taggedpointer_obfuscator的值也是一个ptr类型,系统每次初始化时会对其进行初始化。
当然,也由此知道编码即然是这样做的,那么解码必然是再次与objc_debug_taggedpointer_obfuscator的值进行异或。有代码为证
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
其他
下面,看一看系统是如何针对以上协议来获取TaggedPointer类型的
//objc-internal.h
static inline objc_tag_index_t
_objc_getTaggedPointerTag(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t extTag = (value >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (objc_tag_index_t)(extTag + OBJC_TAG_First52BitPayload);
} else {
return (objc_tag_index_t)basicTag;
}
}
首先,传入TaggedPointer的指针(其实就是个值),然后使用解码函数进行解码。这个解码函数上面已经介绍过了,这里就不再赘述。
其次,使用解码后的value右移60位(_OBJC_TAG_INDEX_SHIFT=60)。这样就获得了高4位的值。然后在与0x7进行与操作(_OBJC_TAG_INDEX_MASK=0x7)。0x7就是0111,这样与value与操作后,等于就要头4位的后3位的值作为basicTag的值。
再次,使用解码后的value右移52位(_OBJC_TAG_EXT_INDEX_SHIFT=52)。这样就获得了高12位的值,然后再与0xff进行与操作(_OBJC_TAG_EXT_INDEX_MASK 0xff)。这就相当于这就相当于只要12位中的后8位。这后8位的值就做为extTag的值。
再次,判断basicTag是不是等于7,如果是,则认定当前TaggedPointer的类型为扩展型(ext)。再之上面介绍过,ext的类型值是被减去8的值。所以这里要加上8然后返回。
最后,如果不等于7,则认为是默认类型的TaggedPointer,直接返回basicTag即可。
缕清楚runtime是如何获取TaggedPointer的类型后,如何获取值也就呼之欲出了。
//objc-internal.h
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
以上两个函数,除了返回值不同,内部取值几乎一样。
- 将传入的TaggedPointer的地址进行解码
- 获取basicTag的值,也就是头4位中后三位的值
- 如果basicTag == 7,则认定为Ext类型的TaggedPointer。然后获取value中后52位的值
- 如果basicTag != 7,则认定为默认类型的TaggedPointer。然后获取value中的后60位的值
最后,再来说说TaggedPointer的存值。以NSString为例,实际上TaggedPointer只能存储9个ASCII码的字符。但自己算下来,64个位,减去头4位,再减去4位的长度,实际上只有56位,ASCII码一个字符占1个字节,也就是8位。那么最多也就存7个字符。结果实验证明可以存9个。这就证明在存储时使用了某些压缩方法,使得9个字符可以存在7个字节里。至于是什么算法,不知道。。。原代码里没找到。
至此,我们基本就介绍完了TaggedPointer类型在runtime中是如何定义,存取值以及它的内存机构是什么样的。打完收工!