目录
Tagged Pointer 的简介
-
程序从 32 位 cpu 迁移到 64 位 cpu 所造成的内存空间浪费
假设要存储一个
NSNumber
对象,其值是一个整数。如果这个整数只是一个NSInteger
类型的普通变量,那么它所占用的内存大小与 cpu 的位数有关:在 32 位 cpu 下占 4 个字节,在 64 位 cpu 下占 8 个字节。并且,指针类型的变量所占用的内存大小也与 cpu 的位数有关,一个指针类型的变量:在 32 位 cpu 下占 4 个字节,在 64 位 cpu 下占 8 个字节// NSObjCRuntime.h #if __LP64__ || 0 || NS_BUILD_32_LIKE_64 typedef long NSInteger; typedef unsigned long NSUInteger; #else typedef int NSInteger; typedef unsigned int NSUInteger; #endif
所以一个普通的 iOS 程序,在没有使用 Tagged Pointer 的情况下,从 32 位 cpu 的机器迁移到 64 位 cup 的机器后,虽然代码没有任何改变,但是
NSNumber
、NSDate
这一类对象所占用的内存会翻倍。如下图所示:
并且采用这种方式表示一个只存储数值的
NSNumber
对象,在效率上也会有问题:为了存储和访问一个NSNumber
对象,程序需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失 -
Tagged Pointer 技术的引入
为了改进上面提到的内存空间占用和程序运行效率的问题,苹果推出了 Tagged Pointer 技术。由于
NSNumber
、NSDate
、NSString
这一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),这对于绝大多数情况都是够用的所以可以将一个对象的指针拆成两部分:
一部分直接保存数据
另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个内存地址引入 Tagged Pointer 技术之后,64 位 cpu 下
NSNumber
的内存图变成了以下这样:
注意:
如果 8 个字节可以承载用于表示的数值时,则系统就会用 Tagged Pointer 的方式生成指针
如果 8 个字节承载不了用于表示的数值时,则系统就会用以前的方式来生成普通的指针 -
Tagged Pointer 技术的特点
以下是苹果在 WWDC2013 《Session 404 Advances in Objective-C》 中对 Tagged Pointer 的介绍(Tagged Pointer 部分从 36:50 左右开始):
① Tagged Pointer 专门用来存储值类型的小内存对象,例如:NSNumber
、NSDate
、NSString
② 因为 Tagged Pointer 指针的值不再是内存地址,而是真正的数据。所以,Tagged Pointer 类型的小内存对象实际上不是一个真正对象,它只是一个披着对象外衣的普通变量而已。所以,Tagged Pointer 类型的小内存对象的内存并不存储在堆中,也不需要malloc
和free
③ Tagged Pointer 类型的小内存对象在内存读取上有着 3 倍的速度提升,在创建和销毁上有着 106 倍的速度提升由此可见,苹果引入 Tagged Pointer 技术后,不但减少了 64 位 cpu 下程序的内存占用,而且提高了 64 位 cpu 下程序的运行效率。完美地解决了值类型的小内存对象在存储和访问效率上的问题
-
示例:NSNumber 对象在未使用 Tagged Pointer 时和使用 Tagged Pointer 时的内存占用
①
NSNumber
对象在未使用 Tagged Pointer 时的内存占用/* 因为 Tagged Pointer 无法禁用,所以以下将 NSInteger 类型的变量 num 设成一个很大的数,以让 NSNumber 类型的 number 对象存储在堆上 @note: 虽然可以在 Target - Run - Arguments - Environment Variables 中添加环境变量 OBJC_DISABLE_TAGGED_POINTERS = YES 来禁用 Tagged Pointer 技术 但是如果这么做了,则程序一运行就会 Crash - objc39337: tagged pointers are disabled (lldb) 因为 RunTime 在程序运行时会判断 Tagged Pointer 是否被禁用,如果 Tagged Pointer 被禁用的话,则会调用 _objc_fatal() 函数杀死当前进程 所以,虽然苹果提供了 OBJC_DISABLE_TAGGED_POINTERS 这个环境变量给开发者,但是 Tagged Pointer 还是无法被禁用 */ #import <malloc/malloc.h> -(void)unusedTaggedPointer { NSInteger num = 0xFFFFFFFFFFFFFF; NSNumber* number = [NSNumber numberWithInteger:num]; NSLog(@"%zd", sizeof(number)); // 8 NSLog(@"%zd", malloc_size((__bridge const void *)(number))); // 32 // 因为 NSNumber 继承自 NSObject,所以 number 对象有 isa 指针,再加上内存对齐的处理,系统给 number 对象在堆中分配了 32 个字节的内存 // 通过 lldb 指令读取 number 对象在堆中的内存,可以看到,实际上 number 对象并没有用完这 32 个字节的内存 // (lldb) p number // (__NSCFNumber *) $0 = 0x00006000032ec020 (long)72057594037927935 // (lldb) memory read 0x00006000032ec020 // 0x6000032ec020: 40 14 d7 86 ff 7f 00 00 83 16 00 00 01 00 00 00 @............... // 0x6000032ec030: ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00 ................ }
在 64 位的 cpu 下,如果没有使用 Tagged Pointer 技术的话,则为了使用一个
NSNumber
对象就需要在栈中开辟 8 个字节的指针内存和在堆中开辟 32 个字节的对象内存。而如果直接使用一个NSInteger
类型的普通变量的话,则只需要在栈中开辟 8 个字节的内存。(使用NSNumber
类型的实例对象的内存开销)比(使用NSInteger
类型的普通变量的内存开销)多了 4 倍!!!②
NSNumber
对象在使用 Tagged Pointer 时的内存占用-(void)usedTaggedPointer { NSInteger num = 0x0; NSNumber* number = [NSNumber numberWithInteger:num]; NSLog(@"%zd", sizeof(number)); // 8 NSLog(@"%zd", malloc_size((__bridge const void *)(number))); // 0 }
在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则
NSNumber
对象的值直接存储在了指针中,系统不会为其在堆上分配内存,可以节省很多内存开销。此时,NSNumber
对象的指针中存储的数据变成了 Tag + Data 的形式(Tag 为特殊标记,用于区分NSNumber
、NSDate
、NSString
等小内存对象的类型;Data 为具体的值)。这样使用一个NSNumber
对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将NSNumber
对象存储到堆中
解除 Tagged Pointer 的数据混淆
macOS 与 iOS 为了保证数据安全,对 Tagged Pointer 类型的指针做了数据混淆,开发者无法通过打印指针的内容来判断一个指针是否为 Tagged Pointer 类型,更无法读取存储在 Tagged Pointer 类型的指针中的数据
为了方便我们在分析 Tagged Pointer 的原理时调试程序,需要先解除系统对 Tagged Pointer 的数据混淆。这里提供 2 种方式:
-
① 通过设置环境变量
系统为开发者提供了环境变量
OBJC_DISABLE_TAG_OBFUSCATION
来控制 Tagged Pointer 数据混淆的禁用和启用。默认情况下,Tagged Pointer 的数据混淆处于启用状态。可以通过在Target - Run - Arguments - Environment Variables
添加环境变量OBJC_DISABLE_TAG_OBFUSCATION
=YES
,来禁用系统对 Tagged Pointer 的数据混淆 -
② 通过还原 RunTime 的混淆函数
Tagged Pointer 的混淆因子是由 RunTime 动态生成的:
// path: objc4-756.2/runtime/objc-runtime-new.mm /* 使用随机数初始化 Tagged Pointer 的混淆因子 objc_debug_taggedpointer_obfuscator Tagged Pointer 的混淆因子 objc_debug_taggedpointer_obfuscator 的主要作用是 使攻击者在发现(缓冲区溢出漏洞)或者(内存写入控制漏洞)时,更难将特定对象构造成 Tagged Pointer 类型的指针 在设置或者获取 Tagged Pointer 所存储的值时 将使用混淆因子 objc_debug_taggedpointer_obfuscator 与 Tagged Pointer 进行异或运算(XOR) 所有的 Tagged Pointer 在第一次使用时就会被随机化 */ static void initializeTaggedPointerObfuscator(void) { if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) || DisableTaggedPointerObfuscation) { // 如果系统版本较老 or 显式设置了环境变量 OBJC_DISABLE_TAG_OBFUSCATION = YES // 则将混淆因子 objc_debug_taggedpointer_obfuscator 设置为 0,即不启用 Tagged Pointer 的数据混淆 objc_debug_taggedpointer_obfuscator = 0; } else { // 1.将随机数存入混淆因子 objc_debug_taggedpointer_obfuscator 中 // 2.将混淆因子 objc_debug_taggedpointer_obfuscator 与取反后的 Tagged Pointer 掩码做 & 运算,并将结果重新存入混淆因子 objc_debug_taggedpointer_obfuscator 中 arc4random_buf(&objc_debug_taggedpointer_obfuscator, sizeof(objc_debug_taggedpointer_obfuscator)); objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; } }
并且在 RunTime 中定义了编码和解码 Tagged Pointer 的函数:
// path: objc4-756.2/runtime/objc-internal.h // 混淆因子 objc_debug_taggedpointer_obfuscator 是个全局变量 extern uintptr_t objc_debug_taggedpointer_obfuscator; // 编码 Tagged Pointer 类型的指针 static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) { return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr); } // 解码 Tagged Pointer 类型的指针 static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; }
虽然我们无法直接使用 RunTime 定义在 objc-internal.h 中的函数
但是我们知道了混淆因子objc_debug_taggedpointer_obfuscator
是个全局变量
并且知道了 RunTime 编码和解码 Tagged Pointer 的过程
因此,我们可以依葫芦画瓢,实现自定义的编码和解码 Tagged Pointer 的函数:// 声明全局变量:混淆因子 objc_debug_taggedpointer_obfuscator extern uintptr_t objc_debug_taggedpointer_obfuscator; // 自定义函数:编码 Tagged Pointer 类型的指针 static inline void * _Nonnull _hcg_objc_encodeTaggedPointer(uintptr_t ptr) { return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr); } // 自定义函数:解码 Tagged Pointer 类型的指针 static inline uintptr_t _hcg_objc_decodeTaggedPointer(const void * _Nullable ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; } // macOS Command-Line App void decode_Tagged_Pointer() { NSNumber* num1 = @1; NSNumber* num2 = @2; NSNumber* num3 = @3; NSLog(@"num1.before = %p, num1.after = 0x%lx", num1, _hcg_objc_decodeTaggedPointer((__bridge const void * _Nullable)(num1))); NSLog(@"num2.before = %p, num2.after = 0x%lx", num2, _hcg_objc_decodeTaggedPointer((__bridge const void * _Nullable)(num2))); NSLog(@"num3.before = %p, num3.after = 0x%lx", num3, _hcg_objc_decodeTaggedPointer((__bridge const void * _Nullable)(num3))); // num1.before = 0xc806440c60b4a8df, num1.after = 0x127 // num2.before = 0xc806440c60b4abdf, num2.after = 0x227 // num3.before = 0xc806440c60b4aadf, num3.after = 0x327 }
Tagged Pointer 的原理:macOS
-
NSNumber 类型的 Tagged Pointer
下图是 macOS 中
NSNumber
的 Tagged Pointer 位视图:
由上图可知,在 macOS 中NSNumber
的 Tagged Pointer 指针中:① 第 0 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer
② 第 1 ~ 3 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的
isa
。类标识位所对应的类对象如下所示:// path: objc4-756.2/runtime/objc-internal.h // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化 // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段 // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段 enum objc_tag_index_t : uint16_t { // 60-bit payloads(拥有 60 位的有效存储空间) 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(索引值 7 为保留字段) OBJC_TAG_RESERVED_7 = 7, ... };
③ 第 4 ~ 7 位是数据类型标识位。当类标识位 ==
OBJC_TAG_NSNumber
时,用于表示NSNumber
中保存的数据类型:数据类型标识位 NSNumber
中保存的数据类型0 char
1 short
2 int
3 long
4 float
5 double
④ 第 8 ~ 63 位是存储数据位。用于存储
NSNumber
的真实数据结合第 ① 点和第 ② 点可知:
对于NSNumber
类型的 Tagged Pointer,它的低 4 位(第 0 ~ 3 位)恒定为 0x7(0111)
可以将这 4 位理解为NSNumber
类型的 Tagged Pointer 的标识
代码验证:
void macOS_tagged_pointer_NSNumber_demo() { NSNumber* num1 = @1; NSNumber* num2 = @2; NSNumber* num3 = @3; NSNumber* num4 = @(0xFFFFFFFFFFFFFFFF); NSLog(@"num1 = %p", num1); // num1 = 0x127 NSLog(@"num2 = %p", num2); // num2 = 0x227 NSLog(@"num3 = %p", num3); // num3 = 0x327 NSLog(@"num4 = %p", num4); // num4 = 0x1004bbe30 // 从以上打印结果可以看出: // num1 ~ num3 的指针为 Tagged Pointer 类型,对象的值都直接存储在了指针当中,对应 0x1、0x2、0x3 // num4 因为数据过大,Tagged Pointer 的存储数据位不够存储,所以内存分配在堆中 // 所有 NSNumber 类型的 Tagged Pointer 都以 0x7 结尾 // 0x7 对应的二进制为 0111 // 最后一位的 1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型 // 前面三位的 011 是类标识位,转换成十进制等于 3,对应 enum objc_tag_index_t 的 OBJC_TAG_NSNumber,表示 NSNumber 类 // 0x127 0x227 0x327 中的 0x2 代表的是什么呢? // 0x2 是数据类型标识位,用于表示 NSNumber 中存储的数据的类型 // 0x0 - char、0x1 - short、0x2 - int、0x3 - long、0x4 - float、0x5 - double char aChar = 'a'; short aShort = 1; int anInt = 1; long aLong = 1; float aFloat = 1.0f; double aDouble = 1.0; NSNumber* numChar = @(aChar); NSNumber* numShort = @(aShort); NSNumber* numInt = @(anInt); NSNumber* numLong = @(aLong); NSNumber* numFloat = @(aFloat); NSNumber* numDouble = @(aDouble); NSLog(@"numChar = %p", numChar); // numChar = 0x6107 NSLog(@"numShort = %p", numShort); // numShort = 0x117 NSLog(@"numInt = %p", numInt); // numInt = 0x127 NSLog(@"numLong = %p", numLong); // numLong = 0x137 NSLog(@"numFloat = %p", numFloat); // numFloat = 0x147 NSLog(@"numDouble = %p", numDouble); // numDouble = 0x157 }
-
NSString 类型的 Tagged Pointer
下图是 macOS 中
NSString
的 Tagged Pointer 位视图:
① 第 0 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer② 第 1 ~ 3 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的
isa
。类标识位所对应的类对象如下所示:// path: objc4-756.2/runtime/objc-internal.h // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化 // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段 // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段 enum objc_tag_index_t : uint16_t { // 60-bit payloads(拥有 60 位的有效存储空间) 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(索引值 7 为保留字段) OBJC_TAG_RESERVED_7 = 7, ... };
③ 第 4 ~ 7 位是数据长度标识位。当类标识位 ==
OBJC_TAG_NSString
时,用于表示NSString
中保存的字符串的长度④ 第 8 ~ 63 位是存储数据位。用于存储
NSString
的真实数据结合第 ① 点和第 ② 点可知:
对于NSString
类型的 Tagged Pointer,它的低 4 位(第 0 ~ 3 位)恒定为 0x5(0101)
可以将这 4 位理解为NSString
类型的 Tagged Pointer 的标识
代码验证:
void macOS_tagged_pointer_NSString_demo() { NSString* str0 = [NSString stringWithFormat:@""]; NSString* str1 = [NSString stringWithFormat:@"1"]; NSString* str2 = [NSString stringWithFormat:@"22"]; NSString* str3 = [NSString stringWithFormat:@"333"]; NSString* str4 = [NSString stringWithFormat:@"4444"]; NSString* str5 = [NSString stringWithFormat:@"55555"]; NSString* str6 = [NSString stringWithFormat:@"666666"]; NSString* str7 = [NSString stringWithFormat:@"7777777"]; NSString* str8 = [NSString stringWithFormat:@"88888888"]; NSString* str9 = [NSString stringWithFormat:@"999999999"]; NSString* stra = [NSString stringWithFormat:@"aaaaaaaaaa"]; NSString* strb = [NSString stringWithFormat:@"bbbbbbbbbbb"]; NSString* strc = [NSString stringWithFormat:@"cccccccccccc"]; NSString* strd = [NSString stringWithFormat:@"ddddddddddddd"]; NSString* stre = [NSString stringWithFormat:@"eeeeeeeeeeeeee"]; NSString* strf = [NSString stringWithFormat:@"fffffffffffffff"]; NSString* strz = [NSString stringWithFormat:@"中文"]; HCGLog(str0); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x7fff90fa90e0, 值 = HCGLog(str1); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3115, 值 = 1 HCGLog(str2); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x323225, 值 = 22 HCGLog(str3); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x33333335, 值 = 333 HCGLog(str4); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3434343445, 值 = 4444 HCGLog(str5); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x353535353555, 值 = 55555 HCGLog(str6); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x36363636363665, 值 = 666666 HCGLog(str7); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3737373737373775, 值 = 7777777 HCGLog(str8); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xaaaaaaaaaaaa85, 值 = 88888888 HCGLog(str9); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x31c71c71c71c7195, 值 = 999999999 HCGLog(stra); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x1084210842108a5, 值 = aaaaaaaaaa HCGLog(strb); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004bfd00, 值 = bbbbbbbbbbb HCGLog(strc); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004bfd20, 值 = cccccccccccc HCGLog(strd); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004c00f0, 值 = ddddddddddddd HCGLog(stre); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004c0110, 值 = eeeeeeeeeeeeee HCGLog(strf); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004c01d0, 值 = fffffffffffffff HCGLog(strz); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004c0130, 值 = 中文 // 通过 [[NSString alloc] initWithFormat:@"xxx"] 创建的 NSString: // 1.当 @"xxx" 的长度为 0 时,是 __NSCFConstantString 类型 // 2.当 @"xxx" 的长度在 1 ~ 10 之间时,是 NSTaggedPointerString 类型 // 3.当 @"xxx" 的长度大于 10 时,是 __NSCFString 类型 // 4.NSTaggedPointerString 类型的字符串引用计数恒定为 -1(对其进行 retain、release 等操作,其引用计数不会发生任何变化) // 5.NSTaggedPointerString 底层对字符串是按照一个字节一个 ASCII 码的方式进行存储,当字符串的长度超过 7 Byte 的时候,NSTaggedPointerString 并没有立即转换为 __NSCFString,而是使用了一种压缩算法进行编码,把字符串的长度进行压缩存储(具体的压缩算法我暂未深究) // 6.当这个压缩算法产生的字符串长度超过 7 Byte 时,才会将 NSTaggedPointerString 转换为 __NSCFString // 7.因为字符串存在很多编码标准,其它语言的字符不能用标准的 ASCII 码来表示。所以对于中文、日文等非 ASCII 字符,即使只有一个字符也用 __NSCFString 进行存储 // 所有 NSString 类型的 Tagged Pointer 都以 0x5 结尾 // 0x5 对应的二进制为 0101 // 最后一位的 1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型 // 前面三位的 010 是类标识位,转换成十进制等于 2,对应 enum objc_tag_index_t 的 OBJC_TAG_NSString,表示 NSString 类 // 0x3115 0x323225 0x33333335 中的 0x1 0x2 0x3 代表的是什么呢? // 0x1 0x2 0x3 是数据长度标识位,用于表示 NSTaggedPointerString 中保存的字符串的长度 // 0x3115 0x323225 0x33333335 中的 0x31 0x3232 0x333333 代表的是什么呢? // 0x31 是字符 a 对应的十六进制 ASCII 码,0x32 是字符 b 对应的十六进制 ASCII 码,0x33 是字符 c 对应的十六进制 ASCII 码 }
Tagged Pointer 的原理: iOS
-
NSNumber 类型的 Tagged Pointer
下图是 iOS 中
NSNumber
的 Tagged Pointer 位视图:
由上图可知,在 iOS 中
NSNumber
的 Tagged Pointer 指针中:① 第 63 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer
② 第 60 ~ 62 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的
isa
。类标识位所对应的类对象如下所示:// path: objc4-756.2/runtime/objc-internal.h // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化 // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段 // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段 enum objc_tag_index_t : uint16_t { // 60-bit payloads(拥有 60 位的有效存储空间) 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(索引值 7 为保留字段) OBJC_TAG_RESERVED_7 = 7, ... };
③ 第 0 ~ 3 位是数据类型标识位。当类标识位 ==
OBJC_TAG_NSNumber
时,用于表示NSNumber
中保存的数据类型:数据类型标识位 NSNumber
中保存的数据类型0 char
1 short
2 int
3 long
4 float
5 double
④ 第 4 ~ 59 位是存储数据位。用于存储
NSNumber
的真实数据结合第 ① 点和第 ② 点可知:
对于NSNumber
类型的 Tagged Pointer,它的高 4 位(第 60 ~ 63 位)恒定为 0xb(1011)
可以将这 4 位理解为NSNumber
类型的 Tagged Pointer 的标识
代码验证:
void iOS_tagged_pointer_NSNumber_demo() { NSNumber* num1 = @1; NSNumber* num2 = @2; NSNumber* num3 = @3; NSNumber* num4 = @(0xFFFFFFFFFFFFFFFF); NSLog(@"num1 = %p", num1); // num1 = 0xb000000000000012 NSLog(@"num2 = %p", num2); // num2 = 0xb000000000000022 NSLog(@"num3 = %p", num3); // num3 = 0xb000000000000032 NSLog(@"num4 = %p", num4); // num4 = 0x280d7b740 // 从以上打印结果可以看出: // num1 ~ num3 的指针为 Tagged Pointer 类型,对象的值都直接存储在了指针当中,对应 0x1、0x2、0x3 // num4 因为数据过大,Tagged Pointer 的存储数据位不够存储,所以内存分配在堆中 // 所有 NSNumber 类型的 Tagged Pointer 都以 0xb 开头 // 0xb 对应的二进制为 1011 // 前面一位的 1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型 // 最后三位的 011 是类标识位,转换成十进制等于 3,对应 enum objc_tag_index_t 的 OBJC_TAG_NSNumber,表示 NSNumber 类 // 0xb000000000000012 0xb000000000000022 0xb000000000000032 中的 0x2 代表的是什么呢? // 0x2 是数据类型标识位,用于表示 NSNumber 中存储的数据的类型 // 0x0 - char、0x1 - short、0x2 - int、0x3 - long、0x4 - float、0x5 - double char aChar = 'a'; short aShort = 1; int anInt = 1; long aLong = 1; float aFloat = 1.0f; double aDouble = 1.0; NSNumber* numChar = @(aChar); NSNumber* numShort = @(aShort); NSNumber* numInt = @(anInt); NSNumber* numLong = @(aLong); NSNumber* numFloat = @(aFloat); NSNumber* numDouble = @(aDouble); NSLog(@"numChar = %p", numChar); // numChar = 0xb000000000000610 NSLog(@"numShort = %p", numShort); // numShort = 0xb000000000000011 NSLog(@"numInt = %p", numInt); // numInt = 0xb000000000000012 NSLog(@"numLong = %p", numLong); // numLong = 0xb000000000000013 NSLog(@"numFloat = %p", numFloat); // numFloat = 0xb000000000000014 NSLog(@"numDouble = %p", numDouble); // numDouble = 0xb000000000000015 }
-
NSString 类型的 Tagged Pointer
下图是 iOS 中
NSString
的 Tagged Pointer 位视图:
① 第 63 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer
② 第 60 ~ 62 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的
isa
。类标识位所对应的类对象如下所示:// path: objc4-756.2/runtime/objc-internal.h // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化 // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段 // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段 enum objc_tag_index_t : uint16_t { // 60-bit payloads(拥有 60 位的有效存储空间) 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(索引值 7 为保留字段) OBJC_TAG_RESERVED_7 = 7, ... };
③ 第 0 ~ 3 位是数据长度标识位。当类标识位 ==
OBJC_TAG_NSString
时,用于表示NSString
中保存的字符串的长度④ 第 4 ~ 59 位是存储数据位。用于存储
NSString
的真实数据结合第 ① 点和第 ② 点可知:
对于NSString
类型的 Tagged Pointer,它的高 4 位(第 60 ~ 63 位)恒定为 0xa(1010)
可以将这 4 位理解为NSString
类型的 Tagged Pointer 的标识
代码验证:
void iOS_tagged_pointer_NSString_demo() { NSString* str0 = [NSString stringWithFormat:@""]; NSString* str1 = [NSString stringWithFormat:@"1"]; NSString* str2 = [NSString stringWithFormat:@"22"]; NSString* str3 = [NSString stringWithFormat:@"333"]; NSString* str4 = [NSString stringWithFormat:@"4444"]; NSString* str5 = [NSString stringWithFormat:@"55555"]; NSString* str6 = [NSString stringWithFormat:@"666666"]; NSString* str7 = [NSString stringWithFormat:@"7777777"]; NSString* str8 = [NSString stringWithFormat:@"88888888"]; NSString* str9 = [NSString stringWithFormat:@"999999999"]; NSString* stra = [NSString stringWithFormat:@"aaaaaaaaaa"]; NSString* strb = [NSString stringWithFormat:@"bbbbbbbbbbb"]; NSString* strc = [NSString stringWithFormat:@"cccccccccccc"]; NSString* strd = [NSString stringWithFormat:@"ddddddddddddd"]; NSString* stre = [NSString stringWithFormat:@"eeeeeeeeeeeeee"]; NSString* strf = [NSString stringWithFormat:@"fffffffffffffff"]; NSString* strz = [NSString stringWithFormat:@"中文"]; HCGLog(str0); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x1fbfb3410, 值 = HCGLog(str1); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000000000311, 值 = 1 HCGLog(str2); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000000032322, 值 = 22 HCGLog(str3); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000003333333, 值 = 333 HCGLog(str4); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000343434344, 值 = 4444 HCGLog(str5); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000035353535355, 值 = 55555 HCGLog(str6); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa003636363636366, 值 = 666666 HCGLog(str7); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa373737373737377, 值 = 7777777 HCGLog(str8); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa00aaaaaaaaaaaa8, 值 = 88888888 HCGLog(str9); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa31c71c71c71c719, 值 = 999999999 HCGLog(stra); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa01084210842108a, 值 = aaaaaaaaaa HCGLog(strb); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x28258abe0, 值 = bbbbbbbbbbb HCGLog(strc); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x28258ac20, 值 = cccccccccccc HCGLog(strd); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x28258ac40, 值 = ddddddddddddd HCGLog(stre); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x28258ac60, 值 = eeeeeeeeeeeeee HCGLog(strf); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x282bd19e0, 值 = fffffffffffffff HCGLog(strz); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x28258ac80, 值 = 中文 // 通过 [[NSString alloc] initWithFormat:@"xxx"] 创建的 NSString: // 1.当 @"xxx" 的长度为 0 时,是 __NSCFConstantString 类型 // 2.当 @"xxx" 的长度在 1 ~ 10 之间时,是 NSTaggedPointerString 类型 // 3.当 @"xxx" 的长度大于 10 时,是 __NSCFString 类型 // 4.NSTaggedPointerString 类型的字符串引用计数恒定为 -1(对其进行 retain、release 等操作,其引用计数不会发生任何变化) // 5.NSTaggedPointerString 底层对字符串是按照一个字节一个 ASCII 码的方式进行存储,当字符串的长度超过 7 Byte 的时候,NSTaggedPointerString 并没有立即转换为 __NSCFString,而是使用了一种压缩算法进行编码,把字符串的长度进行压缩存储(具体的压缩算法我暂未深究) // 6.当这个压缩算法产生的字符串长度超过 7 Byte 时,才会将 NSTaggedPointerString 转换为 __NSCFString // 7.因为字符串存在很多编码标准,其它语言的字符不能用标准的 ASCII 码来表示。所以对于中文、日文等非 ASCII 字符,即使只有一个字符也用 __NSCFString 进行存储 // 所有 NSString 类型的 Tagged Pointer 都以 0xa 开头 // 0xa 对应的二进制为 1010 // 前面一位的 1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型 // 最后三位的 010 是类标识位,转换成十进制等于 2,对应 enum objc_tag_index_t 的 OBJC_TAG_NSString,表示 NSString 类 // 0xa000000000000311 0xa000000000032322 0xa000000003333333 中的 0x1 0x2 0x3 代表的是什么呢? // 0x1 0x2 0x3 是数据长度标识位,用于表示 NSTaggedPointerString 中保存的字符串的长度 // 0xa000000000000311 0xa000000000032322 0xa000000003333333 中的 0x31 0x3232 0x333333 代表的是什么呢? // 0x31 是字符 a 对应的十六进制 ASCII 码,0x32 是字符 b 对应的十六进制 ASCII 码,0x33 是字符 c 对应的十六进制 ASCII 码 }
Tagged Pointer 的判断与内存管理
-
Tagged Pointer 的判断
通过前面的介绍我们知道了:可以通过 Tagged Pointer 的标识位来判断一个指针是否为 Tagged Pointer
并且 RunTime 的源码中提供了相应的函数来判断一个指针是否为 Tagged Pointer:
// path: objc4-756.2/runtime/objc-object.h inline bool objc_object::isTaggedPointer() { return _objc_isTaggedPointer(this); } // path: objc4-756.2/runtime/objc-internal.h // 如果给定的指针 ptr 是一个 tagged pointer 对象,则返回 YES。否则,返回 NO // 不检查给定指针 ptr 所属类的有效性(即不检查类标识位的有效性) static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) { return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; }
可以看到,
_objc_isTaggedPointer
函数的内部实现是将指针ptr
的值与一个宏定义的掩码_OBJC_TAG_MASK
进行按位 & 运算,查看该掩码的定义:#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__ // 64-bit Mac - tag bit is LSB # define OBJC_MSB_TAGGED_POINTERS 0 #else // Everything else - tag bit is MSB # define OBJC_MSB_TAGGED_POINTERS 1 #endif // macOS 下采用 LSB(Least Significant Bit:最低有效位)为 Tagged Pointer 标识位 // iOS 下采用 MSB(Most Significant Bit:最高有效位)为 Tagged Pointer 标识位 #if OBJC_MSB_TAGGED_POINTERS # define _OBJC_TAG_MASK (1UL<<63) // 100000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 ... #else # define _OBJC_TAG_MASK 1UL // 000000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 ... #endif
-
Tagged Pointer 的内存管理
Tagged Pointer 类型的对象,其引用计数恒定为 -1(对其进行
retain
、release
等操作,其引用计数不会发生任何变化)// path: objc4-756.2/runtime/objc-object.h ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { // Tagged Pointer 的 Retain 逻辑 if (isTaggedPointer()) return (id)this; // 非 Tagged Pointer 的 Retain 逻辑 ... } // path: objc4-756.2/runtime/objc-object.h ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) { // Tagged Pointer 的 Release 逻辑 if (isTaggedPointer()) return false; // Tagged Pointer 的 Release 逻辑 ... }
Tagged Pointer 的注意点
-
LSB 与 MSB
macOS 下采用 LSB(Least Significant Bit:最低有效位)为 Tagged Pointer 标识位
iOS 下采用 MSB(Most Significant Bit:最高有效位)为 Tagged Pointer 标识位在 64 位的 macOS 下:
Tagged Pointer 类型的指针最低位为 1,非 Tagged Pointer 类型的指针最低位为 0
因为内存对其机制的存在,64 位的 macOS 在堆空间中会以 8 Byte 进行内存对齐。举个例子:
在 64 位的 macOS 下,Tagged Pointer 的指针是长这样的0x0000000000003115
在 64 位的 macOS 下,普通的指针是长这样的0x00000001004bfd00
综上所述,在 64 位的 macOS 下采用 LSB 并不会干扰到普通指针的使用在 64 位的 iOS 下:
Tagged Pointer 类型的指针最高位为 1,非 Tagged Pointer 类型的指针最高位为 0
虽然 64 位的指针能表示的最大内存地址有 16 EB,但是程序中使用的内存地址在通常情况下远远小于该值。举个例子:
在 64 位的 iOS 下,Tagged Pointer 的指针是长这样的0xa000000000000311
在 64 位的 iOS 下,普通的指针是长这样的0x000000028258ac20
综上所述,在 64 位的 iOS 下采用 MSB 并不会干扰到普通指针的使用 -
Tagged Pointer 与 isa
我们知道,所有 Objective-C 对象都有
isa
指针。因为 Tagged Pointer 并不是真正的 Objective-C 对象,所以 Tagged Pointer 没有isa
指针。如果直接访问 Tagged Pointer 的isa
成员的话,在编译时将会有如下警告:
对于 Tagged Pointer 类型的对象,应该将访问isa
指针转换成相应的方法调用,如:isKindOfClass
方法和object_getClass
函数。只要避免在代码中直接访问 Tagged Pointer 对象的isa
指针,即可避免这个问题当然,现在也不允许在代码中直接访问对象的 isa 指针了,否则编译会不通过
通过 lldb 打印 Tagged Pointer 对象的
isa
指针,会提示如下错误:NSNumber* num1 = @1; (lldb) p num1->isa error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
通过 lldb 打印普通 Objective-C 对象的
isa
指针,是没有问题的:NSNumber* num4 = @(0xFFFFFFFFFFFFFFFF); (lldb) p num4->isa (Class) $1 = __NSCFNumber
-
如何创建 NSString 类型的 Tagged Pointer ?
简单来说,
NSString
是一个采用抽象工厂模式设计的类簇(Class Cluster)。NSString
和NSMutableString
在外部提供了很多方法接口,这些方法接口的实现由具体的内部类来完成。当使用NSString
和NSMutableString
的外部接口生成一个实例对象时,初始化方法会判断哪个内部类最适合生成这个实例对象,然后根据此内部类生成具体的实例对象返回给调用者。不同的创建方式以及不同的字符长度都可能影响最终得到的内部类的类型NSString
和NSMutableString
所使用的内部类如下所示:NSTaggedPointerString
的数据直接存储在了指针中,不需要维护引用计数NSCFConstantString
用于表示字符串常量,存储在字符串常量区,不需要维护引用计数。相同内容的NSCFConstantString
对象的内存地址相同,也就是说字符串常量对象是一种单例对象。NSCFConstantString
对象一般通过字面量@"xxx"
创建NSCFString
存储在堆区,需要维护其引用计数。通过stringWithFormat:
等方法创建的NSString
对象(且字符串长度过长,无法使用 Tagged Pointer 存储)一般都是这种类型
NSString
和NSMutableString
所使用的内部类的继承关系如下所示:NSTaggedPointerString
继承自NSString
NSCFConstantString
继承自NSCFString
继承自NSMutableString
继承自NSString
通过字面量
@"xxx"
创建的NSString
:- 无论字面量
@"xxx"
多长或者多短,都是__NSCFConstantString
类型 - 无论字面量
@"xxx"
是中文或者英文,都是__NSCFConstantString
类型 - 在创建相同内容的字符串时,得到的内存地址相同
__NSCFConstantString
类型的字符串引用计数恒定为 -1(对其进行retain
、release
等操作,其引用计数不会发生任何变化)[[NSString alloc] initWithString:@"xxx"]
、[NSString stringWithString:@"xxx"]
与直接用字面量赋值的结果是一样的,创建的都是__NSCFConstantString
类型的字符串
void NSStringDemo0() { NSString* str0 = @"中文"; NSString* str1 = @"a"; NSString* str2 = @"a"; NSString* str3 = @"abcdefghijklmnopqrstuvwxyz"; NSString* str4 = [[NSString alloc] initWithString:@"a"]; NSString* str5 = [NSString stringWithString:@"a"]; HCGLog(str0); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004150, 值 = 中文 HCGLog(str1); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a HCGLog(str2); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a HCGLog(str3); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004190, 值 = abcdefghijklmnopqrstuvwxyz HCGLog(str4); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a HCGLog(str5); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a }
通过
[[NSString alloc] initWithFormat:@"xxx"]
创建的NSString
:- 当
@"xxx"
的长度为 0 时,是__NSCFConstantString
类型 - 当
@"xxx"
的长度在 1 ~ 10 之间时,是NSTaggedPointerString
类型 - 当
@"xxx"
的长度大于 10 时,是__NSCFString
类型 NSTaggedPointerString
类型的字符串引用计数恒定为 -1(对其进行retain
、release
等操作,其引用计数不会发生任何变化)NSTaggedPointerString
底层对字符串是按照一个字节一个 ASCII 码的方式进行存储- 当字符串的长度超过 7 Byte 的时候,
NSTaggedPointerString
并没有立即转换为__NSCFString
,而是使用了一种压缩算法进行编码,把字符串的长度进行压缩存储(具体的压缩算法我暂未深究) - 当这个压缩算法产生的字符串长度超过 7 Byte 时,才会将
NSTaggedPointerString
转换为__NSCFString
- 因为字符串存在很多编码标准,其它语言的字符不能用标准的 ASCII 码来表示。所以对于中文、日文等非 ASCII 字符,即使只有一个字符也用
__NSCFString
进行存储
void NSStringDemo1() { NSString* str0 = [[NSString alloc] initWithFormat:@""]; NSString* str1 = [[NSString alloc] initWithFormat:@"1"]; NSString* str2 = [[NSString alloc] initWithFormat:@"22"]; NSString* str3 = [[NSString alloc] initWithFormat:@"333"]; NSString* str4 = [[NSString alloc] initWithFormat:@"4444"]; NSString* str5 = [[NSString alloc] initWithFormat:@"55555"]; NSString* str6 = [[NSString alloc] initWithFormat:@"666666"]; NSString* str7 = [[NSString alloc] initWithFormat:@"7777777"]; NSString* str8 = [[NSString alloc] initWithFormat:@"88888888"]; NSString* str9 = [[NSString alloc] initWithFormat:@"999999999"]; NSString* stra = [[NSString alloc] initWithFormat:@"aaaaaaaaaa"]; NSString* strb = [[NSString alloc] initWithFormat:@"bbbbbbbbbbb"]; NSString* strc = [[NSString alloc] initWithFormat:@"cccccccccccc"]; NSString* strd = [[NSString alloc] initWithFormat:@"ddddddddddddd"]; NSString* stre = [[NSString alloc] initWithFormat:@"eeeeeeeeeeeeee"]; NSString* strf = [[NSString alloc] initWithFormat:@"fffffffffffffff"]; NSString* strz = [[NSString alloc] initWithFormat:@"中文"]; HCGLog(str0); // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x7fff90fa90e0, 值 = HCGLog(str1); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3115, 值 = 1 HCGLog(str2); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x323225, 值 = 22 HCGLog(str3); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x33333335, 值 = 333 HCGLog(str4); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3434343445, 值 = 4444 HCGLog(str5); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x353535353555, 值 = 55555 HCGLog(str6); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x36363636363665, 值 = 666666 HCGLog(str7); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3737373737373775, 值 = 7777777 HCGLog(str8); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xaaaaaaaaaaaa85, 值 = 88888888 HCGLog(str9); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x31c71c71c71c7195, 值 = 999999999 HCGLog(stra); // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x1084210842108a5, 值 = aaaaaaaaaa HCGLog(strb); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100607030, 值 = bbbbbbbbbbb HCGLog(strc); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100605e60, 值 = cccccccccccc HCGLog(strd); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100605700, 值 = ddddddddddddd HCGLog(stre); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1006068f0, 值 = eeeeeeeeeeeeee HCGLog(strf); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100607170, 值 = fffffffffffffff HCGLog(strz); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100604f80, 值 = 中文 }
通过
[[NSMutableString alloc] initWithFormat:@"xxx"]
创建的NSMutableString
:- 无论
@"xxx"
为何值,都是__NSCFString
类型 __NSCFString
根据引用计数可以知道其存储在堆上,需要对其进行内存管理
void NSStringDemo2() { NSMutableString* str0 = [[NSMutableString alloc] initWithFormat:@""]; NSMutableString* str1 = [[NSMutableString alloc] initWithFormat:@"1"]; NSMutableString* str2 = [[NSMutableString alloc] initWithFormat:@"fffffffffffffff"]; NSMutableString* str3 = [[NSMutableString alloc] initWithFormat:@"中文"]; HCGLog(str0); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004bdfb0, 值 = HCGLog(str1); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004be370, 值 = 1 HCGLog(str2); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100407610, 值 = fffffffffffffff HCGLog(str3); // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100407660, 值 = 中文 }
一道面试题
-
Question
执行以下两段代码,有什么区别?
@property (nonatomic, strong) NSString* name; -(void)demo1 { dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 1000; i ++) { dispatch_async(queue1, ^{ self.name = [NSString stringWithFormat:@"abcdefghi"]; }); } NSLog(@"end"); }
@property (nonatomic, strong) NSString* name; -(void)demo2 { dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 1000; i ++) { dispatch_async(queue2, ^{ self.name = [NSString stringWithFormat:@"abcdefghij"]; }); } NSLog(@"end"); }
-
Analyse
刚看到这道面试题时,心里一万个草泥马?奔腾而过~~~,两段代码的差别不就是:
demo1
中字符串的长度比demo2
中字符串的长度少了一位吗,哪有什么差别?结果一运行,哎呀!第二段代码居然 Crash 了!而第一段代码却没有问题!奇了怪了?
分别打印两段代码中
self.name
的类型看看:
第一段代码demo1
中self.name
为NSTaggedPointerString
类型
第二段代码demo2
中self.name
为__NSCFString
类型我们再来看一下第二段代码
demo2
Crash 的地方:
想必 Carsh 的原因你已经猜到了:
第二段代码demo2
中的self.name
为__NSCFString
类型,存储在堆上,它是个正常的 Objective-C 对象,需要维护引用计数
self.name = [NSString stringWithFormat:@"abcdefghij"]
是通过name
属性的setter
方法进行赋值的。而name
属性的setter
方法的实现如下所示:-(void)setName:(NSString *)name { if(_name != name) { [_name release]; _name = [name retain]; // or [name copy] } }
程序异步并发执行
name
属性的setter
方法,可能就会有多条线程同时执行[_name release]
,连续对_name
进行两次release
就会造成对象的过度释放,导致 Crash而第一段代码
demo1
中的self.name
为NSTaggedPointerString
类型,在objc_release
函数中会判断指针是不是TaggedPointer
类型,如果是的话则不对对象进行release
操作,也就避免了因过度释放对象而导致的 Crash,因为根本就没执行释放操作 -
Answer
解决方法 ①
将self.name
属性改为用atomic
修饰@property (atomic, strong) NSString* name;
解决方法 ②
将并发执行的任务改为串行执行的任务dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
解决方法 ③
将异步执行改为同步执行dispatch_sync(queue2, ^{ self.name = [NSString stringWithFormat:@"abcdefghij"]; });
解决方法 ④
对赋值过程进行加锁-(void)demo2 { NSLock* locker = [[NSLock alloc] init]; dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 1000; i ++) { dispatch_async(queue2, ^{ [locker lock]; self.name = [NSString stringWithFormat:@"abcdefghij"]; [locker unlock]; }); } NSLog(@"end"); }