Tagged Pointer是什么
我们知道,通常情况下,定义一个变量所占用的内存是与CPU的位数有关,比如NSInteger,在32位CPU下占4个字节,在64位CPU下是占8个字节的。但是他们本身的值并不需要这么多的空间来存储,4个字节能表示的有符号整数就有20多亿。所以苹果推出了Tagged Pointer来减少这种内存浪费,它将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
详情可以看iOS NSString和NSNumber内存分配。这里主要讲下Tagged Pointer的存储策略。
存储策略
来聊一聊tagged pointer是如何存储的。
首先我们创建一些string,并且把它的地址打印出来。
NSMutableString *mutable = [NSMutableString string];
NSString *immutable;
char c = 'a';
for (int i = 0; i < 15; i++) {
[mutable appendFormat:@"%c", c++];
immutable = [mutable copy];
NSLog(@"0x%016lx %@ %@", immutable, immutable, object_getClass(immutable));
}
这里创建了15个字符串,长度从1到15。我们看一下输出:
0xa000000000000611 a NSTaggedPointerString
0xa000000000062612 ab NSTaggedPointerString
0xa000000006362613 abc NSTaggedPointerString
0xa000000646362614 abcd NSTaggedPointerString
0xa000065646362615 abcde NSTaggedPointerString
0xa006665646362616 abcdef NSTaggedPointerString
0xa676665646362617 abcdefg NSTaggedPointerString
0xa0022038a0116958 abcdefgh NSTaggedPointerString
0xa0880e28045a5419 abcdefghi NSTaggedPointerString
0x000060400022f0c0 abcdefghij __NSCFString
0x00006000002210c0 abcdefghijk __NSCFString
0x000060400022ef20 abcdefghijkl __NSCFString
0x0000600000221160 abcdefghijklm __NSCFString
0x000060400022ee60 abcdefghijklmn __NSCFString
0x000060000004e0a0 abcdefghijklmno __NSCFString
我们发现,长度1到9的时候它都是TaggedPointerString
,长度到10以后就是正常的字符串类型了。我们已经知道Tagged Pointer的指针中包含了具体的值,那是怎么保存的呢。我们观察前面这些指针,很容易发现,指针的最后一位数字跟字符串的长度是一致的,指针的第一位都是字母a,并且代表字母的数字是有规律的,a、b、c、d分别对应61、62、63、64。
如果你很熟悉ASCII码,会发现这些数字其实就是对应字母的ASCII码。
苹果在做这些存储时的策略很显而易见了。
- 指针地址首位确定类型,是string、date还是number
- 如果是字符串,最后一位存储字符串长度
- 其余位利用ASCII码编码值来存储字符
那么问题来了,这种存储策略只能存到长度为7的字符串。如上图所示,存到abcdefg
的时候,指针地址是0xa676665646362617
。到长度为8的时候,指针地址变成了0xa0022038a0116958
,已经没有了之前的规律,但是它的类型却依然是TaggedPointerString
。说明苹果在字符串长度大于7的时候改变了策略。
在长度是8和9的时候,苹果采用了6比特的编码,创建了一个table:
eilotrm.apdnsIcufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
我们将abcdefgh
这个字符串的指针拿过来,去掉表示类型的头和表示长度的尾,然后将它转化成如下格式:
0xa0022038a0116958 -> 0022038a011695 ->
001000 100000 001110 001010 000000 010001 011010 010101 ->
8 32 14 10 0 17 26 21
根据最后转化得到的数去查上面的表,可以得到对应的字符串abcdefgh
。
其实TaggedPointerString的最大长度并不是9,虽然我们刚才打印的例子里,到10就不是了。
刚才我们说了8比特编码(ASCII)策略和6比特编码(查表)策略,其实苹果还做了5比特编码策略,也是通过查表的方式,甚至还是刚才的表,只不过更短:
eilotrm.apdnsIcufkMShjTRxgC4013
这个表相对上面那个表要少很多字符,这就决定了不是所有的字符串都可以用5比特编码策略,比如我们前面打印的例子,因为出现了b
是这个表里没有的,所以长度到10的时候就已经不是Tagged Pointer了。如果我们改一下字符串的内容:
NSString *immutable = @"aaaaaaaaaaa";
NSString *copy = [[immutable mutableCopy] copy];
NSLog(@"0x%016lx %@ %@", copy, copy, object_getClass(copy));
//输出
0xa21084210842108b aaaaaaaaaaa NSTaggedPointerString
我们可以看到,长度为10的string依然可以是Tagged Pointer。如果我们做一下同样的转化操作,再去查表,会得到同样的印证。
其他
除了NSString之外,Tagged Pointer还会用在NSNumber、NSDate、NSIndexPath这些类型,这些类型的存储策略相对来说较为简单。比如NSNumber,除了8位标志位外,其余56位则用来存储数值本身内容。当存储用的数值超过56位存储上限的时候,那么NSNumber才会用真正的64位内存地址存储数值,然后用指针指向该内存地址。