TaggedPointer

TaggedPointer是苹果为了在64位架构的处理器下节省内存和提高执行效率而提出的概念。

如果没有TaggedPointer,我们在32位机器中存储一个值为NSInteger类型的NSNumber对象的时候,需要占用8个字节(对象的值+isa指针),而到了64位机器中,就会变成了16个字节(对象的值+isa指针),这里还没算上指向该对象的指针(32位4个字节/64位8个字节)大小。这里引用唐巧的深入理解 Tagged Pointer中的图片

TaggedPointer_第1张图片
内存

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);

打印结果是:
TaggedPointer_第2张图片
what's TP.png

从打印结果中可以看到,前三个为NSTaggedPointerString对象,最后一个却为__NSCFString对象。

TaggedPointer_第3张图片
what's in TP.png

正常的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个实例对象str1str2str3str4的内存地址分别是

0x9c9f66e5c167cd810x9c9f66e5c167cdb10x9c9f66e5c167cda10x2835fd650

因为是在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_obfuscatorobjc_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]);

打印结果为:

class in array.png

这里好像没使用到盐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

class for tag.png

实例对象内存地址的最高位(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);

打印结果为 0x3110x321

先看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查到abcd 对应的ASCII码十六进制分别为 0x610x620x630x64,验证通过。

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);
value in aaaaaaaa.png

当为str为8个a的时候,str对象还是一个TaggedPointer指针,这又是怎么回事?

其实NSTaggedPointerString的3种编解码方式:

  1. 如果长度介于0到7,直接用八位编码存储字符串。
  2. 如果长度是8或9,用六位编码存储字符串,使用编码表eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
  3. 如果长度是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版本

你可能感兴趣的:(TaggedPointer)