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
可以看到a
和b
由明显区别的内存地址。
然后string1
和string2
指向的地址相同,并且都指向abcd
这个字符串的地址。
但是string3
指向地址有一个明显的不一样,这就不得不说我们今天的主角--Tagged Pointer。
Tagged Pointer是什么
我们知道,通常情况下,定义一个变量所占用的内存是与CPU的位数有关,比如NSInteger,在32位CPU下占4个字节,在64位CPU下是占8个字节的。但是他们本身的值并不需要这么多的空间来存储,4个字节能表示的有符号整数就有20多亿。所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。
如下图所示:
不止是内存问题,我们再来看看效率上的问题,为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。所以为了减少这种资源浪费,苹果推出了Tagged Pointer,它将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不是一个内存地址。
来看看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);
看一下不同类型的NSNumber
的Tagged 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倍。