在为了改进从 32位CPU 迁移到 64位CPU 的内存浪费和效率问题,在 64位CPU 环境下,引入了 Tagged Pointer 。旨在提高内存效率和运行性能,尤其针对小的、频繁使用的对象,如NSNumber, NSDate, 和NSString等。在64位处理器上,每个指针占用8字节,而某些对象可能只包含少量数据,因此使用完整对象和指针来管理这些小对象可能造成不必要的内存消耗和性能开销。
以 NSInteger 封装成 NSNumber 为例,内存分布图如下:

在32位处理器中,对象占用的内存有12字节
在64位处理器中,对象占用的内存有24字节,可见翻了一倍

在32位处理器中,对象占用的内存有12字节
在64位处理器中,对象占用的内存只有8字节,节省了4个字节的空间,而且引用计数 retainCount 为最大值。
引用了 Tagged Pointer 的对象,节省了分配在堆区的空间,将值存在指针区域的栈区。从而节省了内存空间以及大大提升了访问速度。
在WWDC2013中苹果介绍了Tagged Pointer有以下特点:
Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
在OC中每个对象都有isa指针,但Tagged Pointer没有,因为Tagged Pointer并不是真正的对象,它没有在堆上分配空间而是直接将值存在指针区域的栈区。如果直接访问Tagged Pointer`
类型的变量的isa指针的话就会在编译时发出警告:

只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。可以使用isKindOfClass 和 object_getClass
运行下面代码:
int main(int argc, const char * argv[]) { @autoreleasepool { NSString *str = [NSString stringWithFormat:@"L"]; NSLog(@"%p - %@ - %@",str,str,str.class); } return 0; } 
通过数据类型可以看出是TaggedPointer,但是它的地址看着十分奇怪这是因为它做了数据混淆。
数据混淆可以防止恶意用户或逆向工程师直接从内存中读取并理解数据,特别是当这些数据包含敏感信息时。
通过查看源码可知异或一个objc_debug_taggedpointer_obfuscator
static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) { //此处进行异或操作 uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr); #if OBJC_SPLIT_TAGGED_POINTERS if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK) return (void *)ptr; uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag); value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT); value |= permutedTag << _OBJC_TAG_INDEX_SHIFT; #endif return (void *)value; } 接着搜索可知这是一个随机值在iOS12之后引入的
static void initializeTaggedPointerObfuscator(void) { if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)) { //此处是随机值 arc4random_buf(&objc_debug_taggedpointer_obfuscator, sizeof(objc_debug_taggedpointer_obfuscator)); objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; #if OBJC_SPLIT_TAGGED_POINTERS objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK); int max = 7; for (int i = max - 1; i >= 0; i--) { int target = arc4random_uniform(i + 1); swap(objc_debug_tag60_permutations[i], objc_debug_tag60_permutations[target]); } #endif } else { objc_debug_taggedpointer_obfuscator = 0; } } 是否开启混淆通过DisableTaggedPointerObfuscation来控制,如果开启了混淆会给objc_debug_taggedpointer_obfuscator赋值一个随机数。如果没有开启混淆会将objc_debug_taggedpointer_obfuscator赋值为0
这里提供两种方法解决数据混淆,第一种是通过解密函数,第二种是改变环境配置
通过上面的源代码不难发现是原来的数据异或一个objc_debug_taggedpointer_obfuscator,如果再异或一次就能得到原来的数据
extern uintptr_t objc_debug_taggedpointer_obfuscator; uintptr_t ssl_objc_decodeTaggedPointer(id ptr) { // 再次异或解密 return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; } int main(int argc, const char * argv[]) { @autoreleasepool { NSString *str = [NSString stringWithFormat:@"L"]; NSLog(@"%p - %@ - %@ -0x%lx",str,str,str.class,ssl_objc_decodeTaggedPointer(str)); } return 0; } 
0x800000000000260b就是我们想要的数据
OBJC_DISABLE_TAG_OBFUSCATION为YES,关闭数据混淆


下面的源码是判断是否为tagged pointer类型:
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) { return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; } #endif 这里用到了_OBJC_TAG_MASK掩码,来看下它的定义:
#if __arm64__ # define OBJC_SPLIT_TAGGED_POINTERS 1 #else # define OBJC_SPLIT_TAGGED_POINTERS 0 #endif #if OBJC_SPLIT_TAGGED_POINTERS // arm64时为 1 # define _OBJC_TAG_MASK (1UL<<63) #else # define _OBJC_TAG_MASK 1UL 在x86-64环境下_OBJC_TAG_MASK的值为1,ptr & 1还是等于1,也就是说指针地址最低位是1的时候,代表这个指针是tagged pointer类型。
下面是类的标志位
// 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, 第1-3位是类标志位
第4-7位表示数据类型或字符串长度

tagged pointer的8-63位用来存储数据

int main(int argc, const char * argv[]) { @autoreleasepool { NSString *str = [NSString stringWithFormat:@"a"]; NSNumber *num1 = @3; NSNumber *num2 = @(0xFFFFFFFFFFFFFFFF); NSLog(@"%p- %p - %p",str,num1,num2); } return 0; } 
#if OBJC_SPLIT_TAGGED_POINTERS # define _OBJC_TAG_MASK (1UL<<63) # define _OBJC_TAG_INDEX_SHIFT 0 # define _OBJC_TAG_SLOT_SHIFT 0 # define _OBJC_TAG_PAYLOAD_LSHIFT 1 # define _OBJC_TAG_PAYLOAD_RSHIFT 4 # define _OBJC_TAG_EXT_MASK (_OBJC_TAG_MASK | 0x7UL) # define _OBJC_TAG_NO_OBFUSCATION_MASK ((1UL<<62) | _OBJC_TAG_EXT_MASK) # define _OBJC_TAG_CONSTANT_POINTER_MASK \ ~(_OBJC_TAG_EXT_MASK | ((uintptr_t)_OBJC_TAG_EXT_SLOT_MASK << _OBJC_TAG_EXT_SLOT_SHIFT)) # define _OBJC_TAG_EXT_INDEX_SHIFT 55 # define _OBJC_TAG_EXT_SLOT_SHIFT 55 # define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 9 # define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 在arm64环境下_OBJC_TAG_MASK的值为 (1UL<<63),ptr & (1UL<<63)还是等于(1UL<<63),也就是说指针地址第63位是1的时候,代表这个指针是tagged pointer类型。
类标志位放在了地址的前三位,即0-2位
数据类型&字符串长度放在地址的3-6位
7到62位用来存储数据

在x86结构中:
8-63位用来存储数据在arm64架构中:
类标志位放在了地址的前三位,即0-2位数据类型&字符串长度放在地址的3-6位7到62位用来存储数据Tagged Pointer可表示的数字范围是-2^55+1 ~ 2^55-1,超出这个范围的数字,NSNumber会转换为普通的Objective-C对象分配在堆上。
Tagged Pointer可表示的字符串范围是9个字符。字符串会转成__NSCFString类型
执行下面两段代码,有什么区别?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 1000; i++) { dispatch_async(queue, ^{ self.name = [NSString stringWithFormat:@"abcdefghij"]; }); } dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 1000; i++) { dispatch_async(queue, ^{ self.name = [NSString stringWithFormat:@"abcdefghi"]; }); } 第一段代码会crash原因是过度释放,第二段代码没问题。两段代码仅差了一个字符。分别打印两段代码的self.name类型,第一段代码中self.name是__NSCFString类型,而第二段代码中为NSTaggedPointerString类型。
第一段代码字符串是__NSCFString存储在堆上,它是个正常对象,需要维护引用计数的。异步并发执行setter方法,可能就会有多条线程同时执行[_name release],连续release两次就会造成对象的过度释放,导致Crash。
第二段代码中的NSString为NSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash