Tagged Pointer

2019-02-28  本文已影响0人  高思阳

这篇文章是参考很多资料才写出来的,有部分内容这几位写的都很详细到位,所以就直接拷贝了,这里向这几位作者学习:
深入理解Tagged Pointer
采用Tagged Pointer的字符串
字面量(Literal)

关于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倍。

image

为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿。所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。

image

于是,简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。在运行时根据实际情况创建。

Tagged Pointer 示例

首先先看NSNumber数值对象

muStr2 = [NSMutableString stringWithString:@"1"];
for(int i=0; i<20; i+=1){
    NSNumber *number = @([muStr2 longLongValue]);
    NSLog(@"%@, %p", [number class], number);
    [muStr2 appendString:@"1"];
}
// 输出结果
__NSCFNumber, 0xb000000000000013
__NSCFNumber, 0xb0000000000000b3
__NSCFNumber, 0xb0000000000006f3
__NSCFNumber, 0xb000000000004573
__NSCFNumber, 0xb00000000002b673
__NSCFNumber, 0xb0000000001b2073
__NSCFNumber, 0xb0000000010f4473
__NSCFNumber, 0xb00000000a98ac73
__NSCFNumber, 0xb000000069f6bc73
__NSCFNumber, 0xb000000423a35c73
__NSCFNumber, 0xb000002964619c73
__NSCFNumber, 0xb000019debd01c73
__NSCFNumber, 0xb000102b36211c73
__NSCFNumber, 0xb000a1b01d4b1c73
__NSCFNumber, 0xb00650e124ef1c73
__NSCFNumber, 0xb03f28cb71571c73
__NSCFNumber, 0xb27797f26d671c73
__NSCFNumber, 0x60000003d540
__NSCFNumber, 0x61000003cb40
__NSCFNumber, 0x61800003c760

数值是1、11、111、1111…..这样递增,可以从输出指针的地址看出最低4位一直为3,这个用于标记是long(float则为4,Int为2,double为5),而最高4位的“b”表示是NSNumber类型;其余56位则用来存储数值本身内容。当存储用的数值超过56位存储上限的时候,那么NSNumber才会用真正的64位内存地址存储数值,然后用指针指向该内存地址。(如果数值长度超过64位,那么就crash)。
因为Tagged Pointed不是一个真正的对象,所以其没有isa。不过只要避免在代码中直接访问对象的isa变量,就没问题。具体如Tagged Pointer 怎么访问类方法列表,之后再详细看下,也许是根据最够为的类型标记,然后调用对应的class方法列表。

再来看看Tagged Pointer String

NSString *str = @"A";
NSString *str2 = [[str mutableCopy] copy];
NSLog(@"str:%p %@", str, str.class);
NSLog(@"str2:%p %@", str2, str2.class);
// 输出结果
str:0x1068a2148 __NSCFConstantString
str2:0xa000000000000411 NSTaggedPointerString

String的TaggedPointer大致和Number一样,最高位表示类型,最低位表示字符串长度,然后字符串内容转为为ASCII码存储(上面的例子A的ASCII为65,转换为16进制是41,而1的ASCII码是49,转换为十六进制则是31)

NSMutableString *muStr2 = [NSMutableString stringWithString:@"1"];
for(int i=0; i<14; i+=1){       
    NSString *strFor = [[muStr2 mutableCopy] copy];
    NSLog(@"%@, %p", [strFor class], strFor);
    [muStr2 appendString:@"1"];
}
// 输出结果
NSTaggedPointerString, 0xa000000000000311
NSTaggedPointerString, 0xa000000000031312
NSTaggedPointerString, 0xa000000003131313
NSTaggedPointerString, 0xa000000313131314
NSTaggedPointerString, 0xa000031313131315
NSTaggedPointerString, 0xa003131313131316
NSTaggedPointerString, 0xa313131313131317
NSTaggedPointerString, 0xa0079e79e79e79e8
NSTaggedPointerString, 0xa1e79e79e79e79e9
NSTaggedPointerString, 0xa03def7bdef7bdea
NSTaggedPointerString, 0xa7bdef7bdef7bdeb
__NSCFString, 0x60000003e7c0
__NSCFString, 0x61800003e5a0
__NSCFString, 0x60800003e2c0

从上面的指针输出可以看出,最低位表示字符串的长度,而其余的56位也是用来存储数组,这里需要注意的是,当字符串内存长度超过了56位的时候,Tagged Pointer并没有立即用指针转向,而是用了一种算法编码,把字符串长度进行压缩存储(具体算法我还不太明白),当这个算法压缩的数据长度超过56位了才使用指针指向。(点击查看具体算法编码,在此谢谢评论区里 _黑苹果 的说明啦)

关于String常量

当String的内容有中文或者特殊字符(非 ASCII 字符)时,那么就只能存储为String指针
但是字面型字符串常量却从不存储为Tagged Pointer。字符串常量必须在不同的操作系统版本下保持二进制兼容,而Tagged Pointer的内部细节是没有保证的。其能使用的前提是Tagged Pointer在运行时总是由Apple的代码生成(运行时才能确定),如果编译器把它们嵌入二进制里(编译),那么前提就被打破了(字符串常量就是这样)。

这里有一段代码,它创建了一个这种字符串并输出它的指针。

NSString *a = @"a";
NSString *b = [[a mutableCopy] copy];
NSLog(@"%p %p %@", a, b, object_getClass(b));

mutableCopy/copy是必要的。原因有两个。首先,尽管像@"a"这样的字符串可以存储为一个Tagged Pointer,但是字符串常量却从不存储为Tagged Pointer。字符串常量必须在不同的操作系统版本下保持二进制兼容,而Tagged Pointer的内部细节是没有保证的。其能使用的前提是Tagged Pointer在运行时总是由Apple的代码生成,如果编译器把它们嵌入二进制里,那么前提就被打破了(字符串常量就是这样)。因此我们需要copy常量字符串来获取Tagged Pointer。

mutableCopy是必要的,因为NSString太聪明,而且也知道一个不可变字符串的副本是一个毫无意义的操作,所以它会返回原字符串的当作“copy”。字符串常量是不可变的,所以[a copy]结果只是a。一个可变量的副本强迫它产生真正副本,这样一个可变量副本的不可变的副本足以让系统给我们产生一个采用Tagged Pointer的字符串。

注意不要在你自己的代码里依赖这些细节!这是NSString的当前情况,它随时可能改变。如果你的代码某种程度上依赖于此,那么代码最终将失效。幸运的是,只有非正常的代码才会这样。所有正常、合理的代码都没有问题,傻傻的不知道任何Tagged Pointer而幸福着吧。

链接:https://www.jianshu.com/p/e354f9137ba8

上一篇下一篇

猜你喜欢

热点阅读