【iOS】Tagged Pointer
文章目录
- 【iOS】Tagged Pointer
- 前言
- 认识Tagged Pointer
- 使用案例
- 结构
- isa指针
- 经典面试题
前言
在之前的学习中笔者在字符串章节简单了解过这个
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);
}
结果:
这里可以看出如果字符串长度在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);}
输出结果
这就是因为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];}
}