【iOS】—— Tagged Pointer

时间:2024-07-20 15:46:34

【iOS】—— Tagged Pointer

    • 关于Tagged Pointer
      • Tagged Pointer的介绍
      • NSTaggedPointer示例
      • NSTaggedPointer结构
      • Tagged Pointer的特点
      • 注意事项
        • isa指针
        • isa指针的优化

关于Tagged Pointer

为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。先看看原有的对象为什么会浪费内存。假设要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。

Tagged Pointer的介绍

在这里插入图片描述

为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

对此提出了Tagged Pointer概念,由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿。所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
在这里插入图片描述
简单来理解就是把指针指向的内容,直接放到了指针变量的内存地址中。 于是使用了标签指针这种方式来优化数据的存储方式。在运行时根据实际情况创建。
在这里插入图片描述

NSTaggedPointer示例

 NSString *string = nil;
 NSMutableString *mutableString = [NSMutableString stringWithFormat:@"abcde"];
 for (int i = 0; i < 13; i++) {
     [mutableString appendString:@"c"];
     string = [mutableString copy];
     NSLog(@"%@ %p %@", string, string, [string class]);
}

输出结果:

在这里插入图片描述
当字符长度在10以内的时候,字符串的类型都是NSTaggedPointer类型,当超过10时,就变成了__NSCFString。

NSTaggedPointer结构

苹果为了安全对其做了编码,runtime内部实现了编码、解码方法,我们看一下:
编码:

 static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr;
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
    return (void *)value;
}

我们可以试着打印地址:

NSNumber *number1 = [NSNumber numberWithInt:1];
NSLog(@"number1 pointer is %p", number1);

输出结果:
在这里插入图片描述

  • Tagged Pointer 标记:x86最后一位是标记位,arm64最高位是标记位。1表示是Tagged Pointer对象,0表示是普通对象。
  • Tag:对象类型标记。x86为13位,arm64为02。7表示有扩展信息。
  • Extended:x86为411位,arm64为5462。用来扩展更多类型。
  • payload:有效负载。存储真正的数据(除了标记位、tag以及extended),不过为了安全苹果做了编码。

Tagged Pointer的特点

  1. Tagged Pointer专门用来存储小的对象,比如NSNumber,NSDate。
  2. Tagged Pointer指针指向的不再是内存地址,而是一个真正的值。所以也不是一个对象,而是披着对象外衣的变量。内存也不存储在堆上,不需要使用malloc和free。
  3. 减少了 64位机器下程序的内存占用,还提高了运行效率,完美地解决了小内存对象在存储和访问效率上的问题。

注意事项

isa指针

Tagged Pointer 的引入也带来了问题,即 Tagged Pointer 并不是真正的对象,而是一个伪对象,所以你如果完全把它当成对象来使用,可能会让它“露马脚”。在上一章中我们写道,所有对象都有isa 指针,而 Tagged Pointer 其实是没有的,因为它不是真正的对象。

isa指针的优化

除了引入Tagged Pointer来优化小的对象,也普通对象的isa指针进行了优化和调整。

在32 位环境下,对象的引用计数都保存在一个外部的表中,每一个对象的 Retain 操作,实际上包括如下 5个步骤:

  1. 获取全局记录引用计数的哈希表。
  2. 为了线程安全,给哈希表上锁。
  3. 找到目标对象的引用计数。
  4. 将引用计数+1,写回哈希表。
  5. 给该哈希表解锁。

为了保证线程安全,对引用计数的增减操作都要先锁定这个表,这从性能上看是非常差的。

在64位的情况下,指针也是64位,实际作为指针部分的只有33位,剩下的31位中,19位用于保存引用计数,当引用计数超过了19位时,才会保存到外部表中,这样引用计数的更改效率提高。
与前面的5个步骤对应,在64位环境下,新的 Retain 操作包括如下 5个步骤:

  1. 检查isa指针是有存在标记位,如果不存在,就执行以前的方法。负责执行的二步。
  2. 判断当前的对象是否正在释放,如果是,就不用进行操作。
  3. 增加对象的引用计数,先不写回isa指针中。
  4. 判断引用计数的位数是否可以被19位表示,如果不能就执行原来的方法,否则执行下一步。
  5. 进行原子的写操作,将isa的值写回。

接下来看一道题

 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
 for (int i = 0; i < 1000; i++) {
     dispatch_async(queue, ^{
        p.name = [NSString stringWithFormat:@"addafghsdddds"];
     });
 }
 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
 for (int i = 0; i < 1000; i++) {
     dispatch_async(queue, ^{
         p.name = [NSString stringWithFormat:@"ad"];
     });
 }

两段代码唯一的区别就是一个name属性所赋值的字符串长一些长度大于10,另一个长度小一点小于10。我们去运行它,就会发现,第一段代码程序崩溃,第二段没有崩溃。
原因就是:
第一段代码并发访问了共享数据 p.name。在多线程环境下,同时对同一变量进行写操作可能引发竞争条件或数据不一致的问题。要解决它就要给它加上锁。
而第二段因为字符串短,所以被改为了Tagged Pointer对象:Tagged Pointer 指针的值不再是地址了,而是真正的值。