TaggedPointer是苹果为了在64位架构的处理器下节省内存和提高执行效率而提出的概念。
如果没有TaggedPointer,我们在32位机器中存储一个值为NSInteger类型的NSNumber对象的时候,需要占用8个字节(对象的值+isa指针),而到了64位机器中,就会变成了16个字节(对象的值+isa指针),这里还没算上指向该对象的指针(32位4个字节/64位8个字节)大小。这里引用唐巧的深入理解 Tagged Pointer中的图片
1.TaggedPointer什么时候会出现?
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];
NSString *str3 = [NSString stringWithFormat:@"c"];
NSString *str4 = [NSString stringWithFormat:@"abcdefghijklmnopqrstuvwxyz"];
NSLog(@"%s %p", object_getClassName(str1), str1);
NSLog(@"%s %p", object_getClassName(str2), str2);
NSLog(@"%s %p", object_getClassName(str3), str3);
NSLog(@"%s %p", object_getClassName(str4), str4);
打印结果是:
从打印结果中可以看到,前三个为NSTaggedPointerString对象,最后一个却为__NSCFString对象。
正常的OC实例对象的第一个成员都是指向类对象内存地址的ISA指针,但是NSTaggedPointerString中却没找到ISA指针。那么NSTaggedPointer又是怎么是去获取类对象的地址的呢?
2.判断是否是TaggedPointer
/* objc-config.h */
// Define SUPPORT_TAGGED_POINTERS=1 to enable tagged pointer objects
// Be sure to edit tagged pointer SPI in objc-internal.h as well.
#if !(__OBJC2__ && __LP64__)
# define SUPPORT_TAGGED_POINTERS 0
#else
# define SUPPORT_TAGGED_POINTERS 1
#endif
/* objc-object.h */
#if SUPPORT_TAGGED_POINTERS
...
inline bool
objc_object::isTaggedPointer()
{
return _objc_isTaggedPointer(this);
}
inline bool
objc_object::isBasicTaggedPointer()
{
return isTaggedPointer() && !isExtTaggedPointer();
}
inline bool
objc_object::isExtTaggedPointer()
{
uintptr_t ptr = _objc_decodeTaggedPointer(this);
return (ptr & _OBJC_TAG_EXT_MASK) == _OBJC_TAG_EXT_MASK;
}
...
// SUPPORT_TAGGED_POINTERS
#else
// not SUPPORT_TAGGED_POINTERS
...
inline bool
objc_object::isTaggedPointer()
{
return false;
}
inline bool
objc_object::isBasicTaggedPointer()
{
return false;
}
inline bool
objc_object::isExtTaggedPointer()
{
return false;
}
...
// not SUPPORT_TAGGED_POINTERS
#endif
TaggedPointer只存在于64位架构,OBJC2.0下,目前用的OC版本就是2.0版本的,并从iPhone 5s开始苹果就才用了64位架构的处理器了。
真正的判断是否TaggedPointer是在下面的函数中:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
这里的宏定义都可以自己下载源码找到,其中TARGET_OS_IOSMAC不是很理解什么意思,但是最后一个条件x86_64很明显,iPhone的是arm64的,所以在iOS真机上判断是否TaggedPointer可以直接看指针的最高一个比特位是否为1
KEY名 | (MacOS 或 TARGET_OS_IOSMAC) 且 _x86_64_ | 其他情况 |
---|---|---|
OBJC_MSB_TAGGED_POINTERS | 0 | 1 |
_OBJC_TAG_MASK | 1UL | 1UL<<63 |
_OBJC_TAG_INDEX_SHIFT | 1 | 60 |
_OBJC_TAG_SLOT_SHIFT | 0 | 60 |
_OBJC_TAG_PAYLOAD_LSHIFT | 0 | 4 |
_OBJC_TAG_PAYLOAD_RSHIFT | 4 | 4 |
_OBJC_TAG_EXT_MASK | 0xfUL | (0xfUL<<60) |
_OBJC_TAG_EXT_INDEX_SHIFT | 4 | 52 |
_OBJC_TAG_EXT_SLOT_SHIFT | 4 | 52 |
_OBJC_TAG_EXT_PAYLOAD_LSHIFT | 0 | 12 |
_OBJC_TAG_EXT_PAYLOAD_RSHIFT | 12 | 12 |
刚刚的4个实例对象str1,str2,str3和str4的内存地址分别是
0x9c9f66e5c167cd81,0x9c9f66e5c167cdb1,0x9c9f66e5c167cda1,0x2835fd650
因为是在64位iOS真机下,所以内存地址是8个字节,要补足64位,不满64位的前面补0。这4个实例对象里面只有str4的内存地址最高位不为1,其他3个实例对象的最高位都为1。所以在64位iOS真机下判断TaggedPointer是没问题的。
3.获取TaggedPointer的类对象
正常的OC对象是通过ISA指针和掩码ISA_MASK进行&运算得到类对象的内存地址的,那么TaggedPointer又是怎样获取类对象的内存地址的呢?
在objc-internal.h文件中我们可以找到可能成为TaggedPointer的类有哪些
#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// 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_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
那么又怎么通过TaggedPointer指针去获取到对应的类呢?在objc-runtime-new.mm文件中找到了两个函数
// Returns a pointer to the class's storage in the tagged class arrays.
// Assumes the tag is a valid basic tag.
static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
>> _OBJC_TAG_INDEX_SHIFT)
& _OBJC_TAG_INDEX_MASK);
uintptr_t obfuscatedTag = tag ^ tagObfuscator;
// Array index in objc_tag_classes includes the tagged bit itself
#if SUPPORT_MSB_TAGGED_POINTERS
return &objc_tag_classes[0x8 | obfuscatedTag];
#else
return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}
// Returns a pointer to the class's storage in the tagged class arrays,
// or nil if the tag is out of range.
static Class *
classSlotForTagIndex(objc_tag_index_t tag)
{
if (tag >= OBJC_TAG_First60BitPayload && tag <= OBJC_TAG_Last60BitPayload) {
return classSlotForBasicTagIndex(tag);
}
if (tag >= OBJC_TAG_First52BitPayload && tag <= OBJC_TAG_Last52BitPayload) {
int index = tag - OBJC_TAG_First52BitPayload;
uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
>> _OBJC_TAG_EXT_INDEX_SHIFT)
& _OBJC_TAG_EXT_INDEX_MASK);
return &objc_tag_ext_classes[index ^ tagObfuscator];
}
return nil;
}
主要看classSlotForBasicTagIndex函数,objc_debug_taggedpointer_obfuscator这个是系统动态运行时创建的盐,每次运行都不一样。64位的iOS真机下,TaggedPointer的类型是放在最高字节的第5~7这3个比特位中。
接下来我们可以自己验证一下,由于objc_debug_taggedpointer_obfuscator和objc_tag_classes都是全局变量,所以我们可以通过extern关键字去使用
#define _OBJC_TAG_SLOT_COUNT 16
#define _OBJC_TAG_EXT_SLOT_COUNT 256
extern "C" {
extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT * 2];
extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
extern uintptr_t objc_debug_taggedpointer_obfuscator;
}
#define objc_tag_classes objc_debug_taggedpointer_classes
#define _OBJC_TAG_INDEX_SHIFT 60
#define _OBJC_TAG_INDEX_MASK 0x7
准备工作已经做好,通过代码验证TaggedPointer的类是否存储在objc_debug_taggedpointer_classes数组中
NSString *str1 = [NSString stringWithFormat:@"a"];
NSNumber *num1 = [NSNumber numberWithInteger:1];
wd_objc_object *wd_str1 = (__bridge wd_objc_object *)str1;
wd_objc_object *wd_num1 = (__bridge wd_objc_object *)num1;
uintptr_t ps1 = (uintptr_t)wd_str1;
uintptr_t pn1 = (uintptr_t)wd_num1;
NSLog(@"%@", objc_tag_classes[ps1>>60]);
NSLog(@"%@", objc_tag_classes[pn1>>60]);
打印结果为:
这里好像没使用到盐objc_debug_taggedpointer_obfuscator,也可以通过TaggedPointer类型进行运算得到类在objc_tag_classes数组中的下标
uint16_t NSString_Tag = 2;
uint16_t NSNumber_Tag = 3;
uintptr_t string_tagObfuscator = ((objc_debug_taggedpointer_obfuscator
>> _OBJC_TAG_INDEX_SHIFT)
& _OBJC_TAG_INDEX_MASK);
uintptr_t number_tagObfuscator = ((objc_debug_taggedpointer_obfuscator
>> _OBJC_TAG_INDEX_SHIFT)
& _OBJC_TAG_INDEX_MASK);
uintptr_t string_obfuscatedTag = NSString_Tag ^ string_tagObfuscator;
uintptr_t number_obfuscatedTag = NSNumber_Tag ^ number_tagObfuscator;
NSLog(@"%@", objc_tag_classes[0x8 | string_obfuscatedTag]);
NSLog(@"%@", objc_tag_classes[0x8 | number_obfuscatedTag]);
打印结果也是NSTaggedPointerString和__NSCFNumber:
实例对象内存地址的最高位(64架构iOS真机)用于判断是否为TaggedPointer,如果为1则为TaggedPointer,接着看后面紧接着的3位去获取TaggedPointer的类型。
4.获取TaggedPointer的值
获取TaggedPointer值的函数在objc-internal.h文件中
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
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指针解码,而解码的函数也很简单,就是和盐objc_debug_taggedpointer_obfuscator进行^运算。
NSString *str1 = [NSString stringWithFormat:@"1"];
NSString *str2 = [NSString stringWithFormat:@"2"];
uintptr_t value1 = objc_getTaggedPointerValue((__bridge void *)str1);
uintptr_t value2 = objc_getTaggedPointerValue((__bridge void *)str2);
NSLog(@"%lx", value1);
NSLog(@"%lx", value2);
打印结果为 0x311,0x321
先看value1的打印结果,可拆分为 0x31 和 0x1,0x31对应的ASCII为数字1,0x1则代表值的长度。可以通过value2的打印结果进行验证,0x32对应的ASCII为数字2,0x1则为值的长度。
可以打印多几组数字进行验证:
[NSString stringWithFormat:@"a"]; // 0x611
[NSString stringWithFormat:@"ab"]; // 0x62612
[NSString stringWithFormat:@"abc"]; // 0x6362613
[NSString stringWithFormat:@"abcd"]; // 0x646362614
ASCII查到a,b,c,d 对应的ASCII码十六进制分别为 0x61,0x62,0x63,0x64,验证通过。
5.TaggedPointer可存储的最大值
按照前面的说法,一个指针占8个字节,64个比特位,第1个比特位用于标记是否为TaggedPointer,第2~4个比特位用于标记TaggedPointer的指针类型,解码后的最后4个比特位用于标记value的长度,那么用于存储value的比特位只有56个比特位(7个字节),也就是说当str为8个a的时候应该就不是TaggedPointer指针了
NSMutableString *muStr = [NSMutableString stringWithCapacity:7];
for (int i = 0; i < 7; i++) {
[muStr appendString:@"a"];
}
NSString *str = muStr.copy;
NSLog(@"%s %p", object_getClassName(str), str);
[muStr appendString:@"a"];
str = muStr.copy;
NSLog(@"%s %p", object_getClassName(str), str);
当为str为8个a的时候,str对象还是一个TaggedPointer指针,这又是怎么回事?
其实NSTaggedPointerString的3种编解码方式:
- 如果长度介于0到7,直接用八位编码存储字符串。
- 如果长度是8或9,用六位编码存储字符串,使用编码表
eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
。 - 如果长度是10或11,用五位编码存储字符串,使用编码表
eilotrm.apdnsIc ufkMShjTRxgC4013
@"aaaaaaaa"
解码后的TaggedPointer值为0x2082082082088,扣除最后4个比特位代表的长度,则为0x20820820820,只有6个字节,但是因为长度为8,需要进行分组解码,6个比特位为一组,分组后为0x0808080808080808,刚好8个字节,长度符合了。采用编码表eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
,下标为8 的刚好是a。
@"aaaaaaaaaa"
解码后的TaggedPointer值为1084210842108a,扣除最后4个比特位代表的长度,则为1084210842108,只有6.5字节,但是因为长度为10,需要进行分组解码,5个比特位为一组,分组后为0x08080808080808080808,刚好10个字节,长度符合了。采用编码表eilotrm.apdnsIc ufkMShjTRxgC4013
,下标为8 的刚好是a。
在编码表中并没有看到
+
字符,使用+
字符做个测试,7个+
应为NSTaggedPointerString,而8个+
则为普通的__NSCFString对象。
NSString *str1 = [NSString stringWithFormat:@"+++++++"];
NSString *str2 = [NSString stringWithFormat:@"++++++++"];
NSLog(@"%s %p", object_getClassName(str1), str1);
NSLog(@"%s %p", object_getClassName(str2), str2);
打印结果为
NSTaggedPointerString 0xff56f63165a49c0e
__NSCFString 0x282f94160
str2指针的最高一个比特位也不为1
了。
6.总结
TaggedPointer给64位系统带来了内存的节省和运行效率的提高,但是TaggedPointer不是真正的对象,且并不是固定说长度多少以下的就是TaggedPointer
参考文章
- 深入理解Tagged Pointer
- 【译】采用Tagged Pointer的字符串
本文采用的源码版本是objc4-750.1版本