iOS Tagged Pointer

这篇文章是参考很多资料才写出来的,有部分内容这几位写的都很详细到位,所以就直接拷贝了,这里向这几位作者学习:
深入理解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一类的对象所占用的内存会翻倍。

直接引用大神的图片

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


直接引用大神的图片

于是,简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 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的代码生成(运行时才能确定),如果编译器把它们嵌入二进制里(编译),那么前提就被打破了(字符串常量就是这样)。

推荐阅读更多精彩内容