1、Tagged Pointer简介

1.1、Tagged Pointer特点

从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储。
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值,对象占16个字节,指针8个字节,就需要至少24个字节,如下图所示:
image.png
使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中(仅用8个字节就可以保存NSNumber的数据),当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

  1. NSNumber *number1 = @4;
  2. NSNumber *number2 = @5;
  3. NSNumber *number3 = @(0xFFFFFFFFFFFFFFFF);
  4. NSLog(@"%p %p %p",number1, number2, number3);

打印结果:

  1. ~: 0x8146ba2e9d0ffff5
  2. ~: 0x8146ba2e9d0ffef5
  3. ~: 0x1058460f0

通过观察打印可以看到,number1、number2都是通过指针来保存的数据,number3是由于指针不过存储数据,使用了动态分配来存储的。

1.2、如何区分Tagged Pointer

区分条件,iOS平台,地址最高有效位是1(第64bit),Mac平台,地址最低有效位是1。
可以查看objc源码中判断是否是TaggedPointer方法:

  1. // iOS平台
  2. # define _OBJC_TAG_MASK (1UL<<63)
  3. // Mac平台
  4. # define _OBJC_TAG_MASK 1UL
  5. // 判断方法
  6. static inline bool
  7. _objc_isTaggedPointer(const void * _Nullable ptr)
  8. {
  9. return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
  10. }

iOS平台位运算:

  1. 通过位运算,拿到最后一个位 _OBJC_TAG_MASK = 1000 0000
  2. 0b 0000 0111
  3. & 0b 1000 0000
  4. ---------------
  5. 0b 1000 0000

Mac平台位运算:

  1. 通过位运算,拿到最后一个位 _OBJC_TAG_MASK = 0b 0000 0001
  2. 0b 0000 0111
  3. & 0b 0000 0001
  4. ---------------
  5. 0b 0000 0001

1.3、Tagged Pointer消息发送

objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

2、Tagged Pointer面试题

2.1、执行以下代码会有什么问题

  1. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  2. for (int i = 0; i < 1000; i++) {
  3. dispatch_async(queue, ^{
  4. self.name = [NSString stringWithFormat:@"abcdefghijk"];
  5. });
  6. }

会报坏内存访问错误: ! Thread 5: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
分析:
因为在给self.name赋值时,实际上调用的是[self setName]方法:

  1. - (void)setName:(NSString *)name {
  2. if (_name != name) {
  3. [_name release];
  4. _name = [name retain];
  5. }
  6. }

有多条线程会同时执行[_name release]方法,所以重复执行release方法就会造成坏内存访问。
解决方案:加锁

2.1、执行以下代码会有什么问题

  1. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  2. for (int i = 0; i < 1000; i++) {
  3. dispatch_async(queue, ^{
  4. self.name = [NSString stringWithFormat:@"abc"];
  5. });
  6. }

如果将字符串改成abc,这段代码就不会崩溃。
分析:
因为abc是通过TaggedPointer方式存储的,是直接赋值,没有调用setName方法,可以通过打印字符串class查看:

  1. NSString *str1 = [NSString stringWithFormat:@"abc"];
  2. NSString *str2 = [NSString stringWithFormat:@"abcdeghijk"];
  3. NSLog(@"%p %p", str1, str2);
  4. NSLog(@"%@ %@", [str1 class], [str2 class]);

打印结果:

  1. ~: 0x83bc89a2bc94c942 0x60000107b2e0
  2. ~: NSTaggedPointerString __NSCFString