文章目录
iOS Tagged Pointer
问题
如果要存一个NSNumber
对象,其值是一个整数。
32位CPU下:指针4位 -> 值4位 (一共需要8位)
64位CPU下:指针8位 -> 值8位 (一共需要16位)(未使用Tagged Pointer
情况下)
这样的数据从 32 位机器迁移到 64 位机器中后,占用的内存会翻倍。为了节省内存和提高执行效率,苹果提出了Tagged Pointer
指针(标记指针)。
原理
将指针(8字节)拆成两部分:一部分直接保存数据,另一部分作为标记(这是一个特别的指针,不指向任何一个地址)
(拿一个整数来说,4个字节所能表示的有符号整数就可达20 多亿,注:2^31=2147483648,另外 1 位作为符号位)
结构
NSNumber
NSString
-
Tagged Pointer
:1表示Tagged Pointer
、0表示非Tagged Pointer
-
类标志位
// objc-internal.h
enum {
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,
OBJC_TAG_RESERVED_7 = 7,
......
};
-
数据类型:(
NSNumber
的)0:char 1:short 2:int 3:long 4:float 5:double
特点
- 专门用来存储小的对象,如:
NSString
、NSNumber
、NSData
- 指针值不再是地址,而是真正的值。(所以,实际上它不再是一个对象了,而是个普通变量而已。因此,它的内存并不存储在堆中,也不需要
malloc
和free
) - 在内存读取上有着3倍的效率,创建时比以前快106倍
当8个字节可以承载用于表示的数值时,系统就会以Tagged Pointer
的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。
测试
测试准备:
在现在的版本中,为了保证数据安全,苹果对 Tagged Pointer
做了数据混淆,开发者通过打印指针无法判断它是不是一个Tagged Pointer
,更无法读取Tagged Pointer
的存储数据。
所以在分析Tagged Pointer
之前,我们需要先关闭Tagged Pointer
的数据混淆,以方便我们调试程序。通过设置环境变量OBJC_DISABLE_TAG_OBFUSCATION
为YES
来关闭。
(设置步骤:Edit Scheme -> Run -> Arguments -> Environment Variables -> 添加key:OBJC_DISABLE_TAG_OBFUSCATION
,value:YES)
NSNumber
NSNumber *num0 = @1;
NSNumber *num1 = @(0xffffffffffffff); // 14个f
// 一共15位(1位4个bit),最高位Tag+类标志,最低位数据类型,所以当大于13个f时就表示不了,需要创建对象了)
NSLog(@"%p", num0); // 0xb000000000000012 (Tagged Pointer 标记指针)
NSLog(@"%p", num1); // 0x6000006965a0 (正常指针)
看num0
的指针:0xb000000000000012
(0x
表示十六进制)
- 最高位 (该例是
b
,转换为二进制是1011)
最高bit位:Tagged Pointer
(该例是1
,表示是Tagged Pointer
)
倒数1-3个bit位:类标志位 (该例是:011
转为十进制是3,对应OBJC_TAG_NSNumber
)
- 最低位:数据类型(该例是
2
,转换为二进制是0010,也就是2,对应int
) - 剩下中间的位:存储数据(该例是
00000000000001
,对应num0
的值1)
NSString
NSString *str1 = [NSString stringWithFormat:@"0"];
NSString *str2 = [NSString stringWithFormat:@"abcdefghij"]; // 存在堆区 (超过9个字符)
NSLog(@"%p %@", str1, [str1 class]);
NSLog(@"%p %@", str2, [str2 class]);
// 0xa000000000000301 NSTaggedPointerString (值直接存储在指针上)
// 0x600003d3c620 __NSCFString (存在堆区)
看str1
的指针:0xa000000000000301
(0x
表示十六进制)
- 最高位 (该例是
a
,转换为二进制是1010)
最高bit位:Tagged Pointer
(该例是1
,表示是Tagged Pointer
)
倒数1-3个bit位:类标志位 (该例是010
,转换十进制是2,对应OBJC_TAG_NSString
)
- 最低位:字符长度(该例是
1
,转换为二进制是0001,十进制也是1,表示字符串长度1) - 剩下中间的位:存储数据(该例是
00000000000030
,转为十进制是48,对应ASCII码表
中的0)
注意事项
isa指针
因为Tagged Pointer
实现的对象,并不是真正的对象,它没有isa
指针,如果直接访问其isa
成员,就会报错
面试题
题1:执行以下两段代码,有什么区别?
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"]; // NSTaggedPointerString (运行正常)
});
}
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"]; // __NSCFString (Crash)
});
}
当字符串设置为@"abcdefghij"
时会crash
,如下:
原因:赋值时会调用setter
方法
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release]; // 异步并发执行setter方法,release就有可能连续执行,造成过度释放
_name = [name copy];
}
}
因为多个赋值是异步的,而且放在了并行队列里。就会创建多个线程同步处理多个赋值操作。release
就有可能连续执行,造成过度释放。
而当字符少于10个时,系统采用了Tagged Pointer
机制将数据直接存储在指针上。 objc_release
内部会判断,如果是Tagged Pointer
则不会进行release
,直接赋值。所以不会导致过度释放的BAD_ACCESS
错误。
__attribute__((aligned(16), flatten, noinline))
void objc_release(id obj) {
if (!obj) return;
if (obj->isTaggedPointer()) return; // 如果是TaggedPointer则不会进行release
return obj->release();
}
解决方案:
-
方法1:使用串行队列
-
方法2:使用
atomic
-
方法3:赋值方法前后:加锁、解锁
// 加锁 self.name = [NSString stringWithFormat:@"abcdefghij"]; // 解锁
参考:
iOS - 老生常谈内存管理(五):Tagged Pointer(Mac OS + iOS 下 NSNumber + NSString 的Tagged Pointer 结构图)