如果要存一个NSNumber
对象,其值是一个整数。
32位CPU下:指针4位 -> 值4位 (一共需要8位)
64位CPU下:指针8位 -> 值8位 (一共需要16位)(未使用Tagged Pointer
情况下)
这样的数据从 32 位机器迁移到 64 位机器中后,占用的内存会翻倍。为了节省内存和提高执行效率,苹果提出了Tagged Pointer
指针(标记指针)。
将指针(8字节)拆成两部分:一部分直接保存数据,另一部分作为标记(这是一个特别的指针,不指向任何一个地址)
(拿一个整数来说,4个字节所能表示的有符号整数就可达20 多亿,注:2^31=2147483648,另外 1 位作为符号位)
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,
......
};
NSString
、 NSNumber
、NSData
malloc
和free
)当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 *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 *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)因为Tagged Pointer
实现的对象,并不是真正的对象,它没有isa
指针,如果直接访问其isa
成员,就会报错
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();
}
解决方案:
atomic
// 加锁
self.name = [NSString stringWithFormat:@"abcdefghij"];
// 解锁
参考:
iOS - 老生常谈内存管理(五):Tagged Pointer(Mac OS + iOS 下 NSNumber + NSString 的Tagged Pointer 结构图)
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有