iOS_Tagged Pointer是什么,结构

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

    0char
    1short
    2int
    3long
    4float
    5double
    

特点

  • 专门用来存储小的对象,如:NSStringNSNumberNSData
  • 指针值不再是地址,而是真正的值。(所以,实际上它不再是一个对象了,而是个普通变量而已。因此,它的内存并不存储在堆中,也不需要mallocfree
  • 在内存读取上有着3倍的效率,创建时比以前快106倍

当8个字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。


测试

测试准备:

在现在的版本中,为了保证数据安全,苹果对 Tagged Pointer 做了数据混淆,开发者通过打印指针无法判断它是不是一个Tagged Pointer,更无法读取Tagged Pointer的存储数据。

所以在分析Tagged Pointer之前,我们需要先关闭Tagged Pointer的数据混淆,以方便我们调试程序。通过设置环境变量OBJC_DISABLE_TAG_OBFUSCATIONYES来关闭。

(设置步骤: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的指针:0xb0000000000000120x表示十六进制)

  • 最高位 (该例是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的指针:0xa0000000000003010x表示十六进制)

  • 最高位 (该例是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"];
    // 解锁
    

参考:

深入理解Tagged Pointer

iOS - 老生常谈内存管理(五):Tagged Pointer(Mac OS + iOS 下 NSNumber + NSString 的Tagged Pointer 结构图)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小莫同学~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值