Tagged Pointer内存管理

Object-C是怎么分配内存的?

内存地址
  • 代码区 代码段是用来存放可执行文件的操作指令(存放函数的二进制代码),也就是说是它是可执行程序在内存种的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作--它是不可写的。

  • 常量区 常量区存放的就是字符串常量,int常量等这些常量。

  • 全局区 全局/静态区存放的是静态变量,静态全局变量,以及全局变量。初始化的全局变量,静态变量,静态全局变量存放在同一区域,未初始化的变量存放在相邻的区域。程序结束后由系统释放。

  • 堆区 堆区内存一般是由程序员自己分配并释放的。当我们使用alloc来分配内存时分配的内存就是在堆上。由于我们现在大部分都是使用ARC,所以现在堆区的分配和释放也基本不需要我们来管理。堆区是向高地址扩展的一块非连续区域。

  • 栈区 这块区域是由编译器自动分配并释放的,栈区存放的是函数的参数及自动变量。栈是向低地址扩展的一块连续的内存区域。分配在栈上的变量,当函数的作用域结束,系统就会自动销毁变量。

先来看一段内存分配的代码

    static NSInteger a = 3;//静态局部变量分配在全局/静态区
    NSInteger b = 4;      //自动变量分配在栈区
    NSLog(@"\n &a: %p \n &b: %p\n", &a, &b);
    NSString *string1 = @"abcd";//string1自动变量分配在栈区,后面的字符串常亮分配在常量区
    NSString *string2 = [[NSString alloc]initWithString:@"abcd"]; // string2自动变量分配在栈区,后面的字符串常亮分配在常量区
    NSString *string3 = [NSString stringWithFormat:@"abcd"]; //Format初始化的NSString类型不是普通的__NSCFString类型,就是我们今天要讲的主角--TaggedPointer,这样初始化出来的是NSTaggedPointerString类型的字符串
    NSLog(@"\n abcd : %p \n string1 : %p &string1 : %p  \n string2 : %p &string2 : %p\n string3 : %p &string3 : %p\n\n\n\n", @"abcd", string1, &string1, string2, &string2, string3, &string3);

这些代码就是简单的看一下不同类型的变量的内存地址

&a:  0x10aeef0a8 
&b:  0x7ffee4d16528
abcd :    0x10aeed2c8 
string1 : 0x10aeed2c8          &string1 : 0x7ffee4d16520  
string2 : 0x10aeed2c8          &string2 : 0x7ffee4d16518
string3 : 0xa000000646362614   &string3 : 0x7ffee4d16510

可以看到ab由明显区别的内存地址。
然后string1string2指向的地址相同,并且都指向abcd这个字符串的地址。

但是string3指向地址有一个明显的不一样,这就不得不说我们今天的主角--Tagged Pointer。

Tagged Pointer是什么

我们知道,通常情况下,定义一个变量所占用的内存是与CPU的位数有关,比如NSInteger,在32位CPU下占4个字节,在64位CPU下是占8个字节的。但是他们本身的值并不需要这么多的空间来存储,4个字节能表示的有符号整数就有20多亿。所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。
如下图所示:


内存迁移.png

不止是内存问题,我们再来看看效率上的问题,为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。所以为了减少这种资源浪费,苹果推出了Tagged Pointer,它将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不是一个内存地址。

Tagged Pointer内存.png

来看看Tagged Pointe具体怎么工作的

    NSMutableString *mutable = [NSMutableString string];
    NSString *immutable;
    char c = 'a';
    for (int i = 0; i < 15; i++) {
          [mutable appendFormat:@"%c", c];
          immutable = [mutable copy];
        NSLog(@"%@--- %@-- %p", [immutable class], immutable, immutable);
     }

然后看一下控制台的输出信息,我们可以看到并不是每一次都是NSTaggedPointerString类型,这是因为Tagged Pointer只有8个字节,不可能无限制的存储任何字符串的。
而且大家看一下每次打印NSTaggedPointerString类型的最后一位,有没有发现是不是刚好和字符串的长度一样呢,而第一位永远是a。而61又刚好对应a的16进制的值。

NSTaggedPointerString--- a-- 0xa000000000000611
NSTaggedPointerString--- aa-- 0xa000000000061612
NSTaggedPointerString--- aaa-- 0xa000000006161613
NSTaggedPointerString--- aaaa-- 0xa000000616161614
NSTaggedPointerString--- aaaaa-- 0xa000061616161615
NSTaggedPointerString--- aaaaaa-- 0xa006161616161616
NSTaggedPointerString--- aaaaaaa-- 0xa616161616161617
NSTaggedPointerString--- aaaaaaaa-- 0xa002082082082088
NSTaggedPointerString--- aaaaaaaaa-- 0xa082082082082089
NSTaggedPointerString--- aaaaaaaaaa-- 0xa01084210842108a
NSTaggedPointerString--- aaaaaaaaaaa-- 0xa21084210842108b
__NSCFString--- aaaaaaaaaaaa-- 0x7f8e7dd12520
__NSCFString--- aaaaaaaaaaaaa-- 0x7f8e7df765b0
__NSCFString--- aaaaaaaaaaaaaa-- 0x7f8e7df00180
__NSCFString--- aaaaaaaaaaaaaaa-- 0x7f8e7dd1a180

Tagged Pointer就是就是这样一个特殊的值,第一位是一个特殊标记,代表着当前的Tagged Pointer是一个什么类型的值,最后一位也是一个标记,如果Tagged Pointer是字符串类型,最后一位就代表自负串长度,而中间的2-15位就是真实的值。

再来看一下别的Tagged Pointer值,看看有什么区别和共同点:

    char c = 'a';
    NSString *string = @"1111";
    NSNumber *number = @([string intValue]);
    NSLog(@"%@--- %@-- %p", [number class], number, number);
    
    NSNumber *number1 = @([string integerValue]);
    NSLog(@"%@--- %@-- %p", [number1 class], number1, number1);
    
    NSNumber *number2 = @([string floatValue]);
    NSLog(@"%@--- %@-- %p", [number2 class], number2, number2);
    
    NSNumber *number3 = @([string doubleValue]);
    NSLog(@"%@--- %@-- %p", [number3 class], number3, number3);
    
    NSNumber *number4 = @(-1111);
    NSLog(@"%@--- %@-- %p", [number4 class], number4, number4);
    
    NSNumber *number5 = @(YES);
    NSLog(@"%@--- %@-- %p", [number5 class], number5, number5);
    
    NSNumber *number6 = @(c);
    NSLog(@"%@--- %@-- %p", [number6 class], number6, number6);

看一下不同类型的NSNumberTagged Pointer会有什么不同呢?
我们会发现第一个标志位全部变成了b,然后最后一位不再是值的长度,而是代表了NSNumber不同的类型。
BOOL值似乎又有些不一样,不是一个__NSCFNumber的类型,而是一个__NSCFBoolean,也不再是Tagged Pointer的值类型。

__NSCFNumber--- 1111-- 0xb000000000004572
__NSCFNumber--- 1111-- 0xb000000000004573
__NSCFNumber--- 1111-- 0xb000000000004574
__NSCFNumber--- 1111-- 0xb000000000004575
__NSCFNumber--- -1111-- 0xbfffffffffffba92
__NSCFBoolean--- 1-- 0x10fdd8518
__NSCFNumber--- 97-- 0xb000000000000610

从上面两个例子我们大概能得出一些结论:

  • 指针地址首位确定类型,是string、date还是number
  • 对应不同的类型,最后一位也有不同的意义
  • 2-15位利用ASCII码编码值来存储真实的值

总结一下Tagged Pointer的特点

1.我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
Tagged Pointer专门用来存储小的对象,例如NSNumber、NSString和NSDate等等。

2.Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。

3.在内存读取上有着3倍的效率,创建时比以前快106倍。

你可能感兴趣的:(Tagged Pointer内存管理)