文章目录
- 关于Tagged Pointer
- NSTaggedPointer示例
- TaggedPointer 结构
- Tagged Pointer特点
- 注意事项
- isa指针
- 64位下的isa指针优化
本来打算细看一下weak的底层原理,看到了出现了很多次Tagged Pointer对象,就先来学一下Tagged Pointer,在之前刚学习OC的时候,学习NSString三种类型的时候,接触过这个类型,但是没有往下细看,现在来细看看:
关于Tagged Pointer
在2013年9月,苹果推出了iPhone5s,与此同时,iPhone5s配备了首 个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了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
特点的介绍:
Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
。
在内存读取上有着3倍的效率,创建时比以前快106倍。
为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿。所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
于是,简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。在运行时根据实际情况创建。
NSTaggedPointer示例
NSString *string = nil;
NSMutableString *mutableString = [NSMutableString stringWithFormat:@"abcde"];
for (int i = 5; i < 15; i++) {
[mutableString appendFormat:@"a"];
string = [mutableString copy];
NSLog(@"%@ %p %@", string, string, [string class]);
}
输出结果:
当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString
TaggedPointer 结构
苹果为了安全对其做了编码,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);
输出结果:
number1 pointer is 0xbb027d74df7d32ea
可见,这个地址是被编码过的。通过资料的查询,我们可以对其结构有个了解:
- Tagged Pointer 标记:x86最后一位是标记位,arm64最高位是标记位。1表示是Tagged Pointer对象,0表示是普通对象。
- Tag:对象类型标记。x86为13位,arm64为02。7表示有扩展信息。
- Extended:x86为411位,arm64为5462。用来扩展更多类型。
- payload:有效负载。存储真正的数据(除了标记位、tag以及extended),不过为了安全苹果做了编码。
Tagged Pointer特点
我们也可以看到苹果对于Tagged Pointer 特点的介绍:
- Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate。
- Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象
了,它只是一个披着对象“皮”的普通变量而己。所以,它的内存并不存储在堆中,也
不需要 malloc 和free。 - 在内存读取上有着以前3倍的效率,创建时比以前快 106倍
因此,苹果引入 Tagged Pointer,不但减少了 64位机器下程序的内存占用,还提高了运行效率,完美地解决了小内存对象在存储和访问效率上的问题。
注意事项
isa指针
Tagged Pointer 的引入也带来了问题,即 Tagged Pointer 并不是真正的对象,而是一个伪对象,所以你如果完全把它当成对象来使用,可能会让它“露马脚”。在上一章中我们写道,所有对象都有isa 指针,而 Tagged Pointer 其实是没有的,因为它不是真正的对象。
64位下的isa指针优化
对于 64位设备,苹果除了引人 Tagged Pointer 来优化小的对象外,对于普通的对象,其isa指针也进行了优化和调整。在32 位环境下,对象的引用计数都保存在一个外部的表中,每一个对象的 Retain 操作,实际上包括如下 5个步骤:
- 获得全局的记录引用计数的hash 表。
- 为了线程安全,给该hash 表加锁。
- 查找到目标对象的引用计数值。
- 将该引用计数值加1,写回hash 表。
- 给该hash 表解锁。
从上面的步骤我们可以看出,为了保证线程安全,对引用计数的增减操作都要先锁定这个表,这从性能上看是非常差的。
而在 64 位环境下,isa
指针也是64 位,实际作为指针部分只用到的其中33位,剩余的 31位苹果使用了类似 Tagged Pointer 的概念,其中 19 位将保存对象的引1用计数,这样对引用计数的操作只需要修改这个指针即可。只有当引用计数超出19位,才会将引用计数保存到外部表,而这种情况是很少的,所以这样引用计数的更改效率会更高。
与前面的5个步骤对应,在64位环境下,新的 Retain 操作包括如下 5个步骤:
- 检查
isa
指针上面的标记位,看引1用计数是否保存在 isa变量中,如果不是,则使用以前
的步骤,否则执行第2步 - 检查当前对象是否正在释放,如果是,则不做任何事情。
- 增加该对象的引用计数,但是并不马上写回到
isa
变量中。 - 检杳增加后的引用计数的值是否能够被 19 位表示,如果不是,则切换成以前的办法,否则执行第5步。
- 进行一个原子的写操作,将
isa
的值写回。
虽然步骤都是 5步,但是由于没有了全局的加锁操作,所以引用计数的更改更快了。