【iOS】Tagged Pointer

发布于:2025-05-12 ⋅ 阅读:(12) ⋅ 点赞:(0)

【iOS】Tagged Pointer

前言

在之前的学习中笔者在字符串章节简单了解过这个Tagged Pointer后面笔者就没在多了解这部分内容,今天决定比较系统的学习一下有关于这部分内容的知识.

认识Tagged Pointer

标记指针(Tagged Pointer)是一种优化技术,用于在不分配额外内存的情况下存储小的对象或数字值。在这种技术中,指针的最低有效位LSB)用于存储特殊标记,而不是指向分配的内存地址。

在传统上,OC对象都是通过指针引用的,指针指向一个存储在堆内存中的实例对象.然而小对象在64位环境下就明明只占用了很小的一个内存空间,但是却占用了很大内存空间,为了节约内存和提高性能,OC引入了标记指针.也就是Tagged Pointer

Tagged Pointer通过修改指针的最低有效位来存储伊谢尔比较简单的值,如整数,浮点数和布尔值.这样就可以提高我们程序的性能和内存利用率.

  • 优势:

Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
在内存读取上有着3倍的效率,创建时比以前快106倍。

为了存储或者访问一个NSNumber对象.我们需要在堆上维他分配内存,另外还要维护它的一个引用计数,管理生命周期

在这里插入图片描述

为了改进上面提到内存浪费的问题和效率问题,苹果提出了Tagged Pointer对象,由于NSNumber这种类型的变量值需要占用的内存大小常常不需要8个字节,所以我们可以讲一个对象的指针拆分成两个部分,一部分直接保存数据,另一个部分作为特殊标记,表示这是一个特别的指针.

在这里插入图片描述

使用案例

NSMutableString* stirng = [@"ab" mutableCopy];
for (int i = 0; i <= 15; i++) {
     [stirng appendFormat:@"c"];
      NSLog(@"%@ %@ %ld", [stirng copy], [[stirng copy] class], stirng.length);
}

结果:

image-20250506211650202

这里可以看出如果字符串长度在10个以内,字符串的类型就是我们的NSTaggedPointerString超过10个的时候才是__NSCFString

刚刚使用的时候,我们发现这里的string进行了一次不可便拷贝,我们如果把这次不可变拷贝去掉,我们在来看一下这个函数

NSMutableString* stirng = [@"ab" mutableCopy];
        for (int i = 0; i <= 15; i++) {
            [stirng appendFormat:@"c"];
            NSLog(@"%@ %@ %ld", stirng , [stirng class], stirng.length);
        }

输出结果

image-20250506212133137

这就是因为Tagged Pointer主要用于优化小的而且不可变的对象.

如果我们按照这种方式创建字符串的话

NSString *str = @"abcde";
NSLog(@"%@ %p", [str class], str);

__NSCFConstantString 0x100004318
Type: Notice | Timestamp: 2025-05-06 22:08:21.832109+08:00 | Process: GGDebug | Library: GGDebug | TID: 0x3b78fa

这是因为__NSCFConstantString是编译时确定的字符串,它们的值和内存地址在程序运行期间是不会改变的。而str在编译时就已经确定了,它的值和内存地址在程序运行期间是不会改变的,所以其类型是__NSCFConstantString,而不是NSTaggedPointerString。

结构

结构大致如下:

| 1bit | 3~4bits |     payload (60bits)      |
|  T   |  tag    |         data              |

Tagged Pointer 标记:这是用来标记该对象是否为Tagged Pointer对象的标志位。在macOS(x86)中,这个标识位是最后一位;在iOS(arm64)中,这是最高位。1表示是Tagged Pointer对象,0表示是普通对象。
Tag:这是对象类型的标记。在(macOS)x86架构中,它占据3位;在(iOS)arm64架构中,它占据2位。其中,值为7表示有扩展信息。
Extended:这部分用于扩展更多类型。在x86架构中,它占据4位;在arm64架构中,它占据5位。
payload:这是有效负载,用于存储真正的数据(除了标记位、tag以及extended)。但是为了安全,苹果对其进行了编码。

isa指针

Tagged Pointer的引入也带来了问题,即Tagged Pointer因为并不是真正的对象,而是一个伪对象,所以所有对象都有 isa 指针,而Tagged Pointer其实是没有的,因为它不是真正的对象。’

  • 苹果将Tagged Pointer引入,给 64 位系统带来了内存的节省和运行效率的提高。
  • Tagged Pointer通过在其最后一个 bit 位设置一个特殊标记,用于将数据直接保存在指针本身中。因为Tagged Pointer并不是真正的对象,我们在使用时需要注意不要直接访问其 isa 变量。

在32位环境下,对于每一个对象的引用计数都保存在一个外部的表中,每个对象的引用计数都保存在外部的一个表中,它对于每一个对象的持有操作是这样实现的:

  • 获取全局的记录引用计数的hash表
  • 为了线程安全,给该hash表加锁
  • 查找到对应对象的一个引用计数值
  • 将该引用计数加1,写回hash表
  • 给这个表解锁

从上面的这五个步骤可以看出,为了保证引用计数的增减操作都要先锁定这个表,在新的64位的环境下,isa的指针也变成了64位,其中有31位采用了类似与Tagged Pointer的一个概念,其中19位用来保存对象的引用计数,这样对引用计数的操作只用修改这个指针就可以了.只有引用计数的大小超过19位的时候,才会把引用计数保存到外部表.这样就有新的步骤:

  • 检查 isa 指针上面的标记位,看引1用计数是否保存在 isa变量中,如果不是,则使用以前的步骤,否则执行第2步
  • 检查当前对象是否正在释放,如果是,则不做任何事情
  • 增加该对象的引用计数,但是并不马上写回到isa 变量中。
  • 检杳增加后的引用计数的值是否能够被 19 位表示,如果不是,则切换成以前的办法,否则执行第5步。
  • 进行一个原子的写操作,将isa 的值写回。

经典面试题

下面两段代码的一个运行结果会是什么?

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (NSInteger i = 0; i < 1000; i ++) {
        dispatch_async(queue, ^{
           
            self.string = [NSString stringWithFormat:@"111123123123123"];
        });
    }


dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (NSInteger i = 0; i < 1000; i ++) {
        dispatch_async(queue, ^{
           
            self.string = [NSString stringWithFormat:@"11112"];
        });
    }

这两个代码唯一的区别在于一个name属性的字符串大于10,.一个小于10.在运行的时候会发现第一段代码崩溃,第二段代码可以正常运行:

第一段代码会报错的是坏内存访问

原因:

我们知道所有的属性实际上是这样实现的

  - (void)setString:(NSString *)string{
	if (_string != string) {
    	[_string release];
   	 _string = [string copy];
	}
}

这里因为前面一致是我们的堆上创建的字符串,导致它的一直进入函数里面执行realse方法,这样可能会导致多个线程同时调用realse方法,但是在第二个线程调用的时候string以及被释放了,所有就会出现一个坏内存报错:

解决方法:

  • 用atomic修饰我们的string
@property (atomic, copy) NSString* string;
  • 在外部修改stirng赋值的时候加上互斥锁
- (void)setString:(NSString *)string {
    @synchronized (self) {
        _string = [string copy];
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到