一、iOS 应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在几个不同的内存区域
1. 代码区
代码区是用来存放函数的二进制代码(存放App代码),它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只允许读取操作,而不允许写入操作。
2. 全局(静态)区
- 数据区:数据段用来存放可执行文件中已经初始化的全局变量,也就是用来存放静态分配的变量和全局变量。
- BSS区:BSS段包含了程序中未初始化的全局变量
包括两个部分:未初始化过 、初始化过; 也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域; eg:int a;未初始化的。int a = 10;已初始化的。
3. 常量区
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量。
4. 堆(heap)区
堆是由程序员分配和释放(若程序员不释放,则可能会引起内存泄漏),用于存放进程运行中被动态分配的内存段。它大小不固定,可动态扩张和缩减。
- 堆区的内存分配使用的是alloc;
- ARC的内存的管理,是编译器在编译的时候自动添加 retain、release、autorelease;
- 堆区的地址是从低到高
5. 栈(stack)区
栈是由编译器自动分配释放来管理内存。用户存放程序临时创建的变量、存放函数的参数值、局部变量等。由于栈的先进后出特点,所以特别适合用来做保存/恢复现场的操作。从这个意义上,我们可以把栈看做一个临时寄存、交换的内存区。
上述几种内存区域中,数据段、BSS、堆通常都是被连续存储的-内存位置上的连续(并不是堆链式存储的内存区域)。而代码段和栈往往会被独立存放。
栈是向低地址扩展的数据结构,是一块连续的内存区域。堆是向高地址扩展的数据结构,是不连续的内存区域。
二、内存模型 Heap、Stack
2.1、堆 Heap
寄存器(栈)只能存放少量的数据,大多数时候, CPU
还要指挥寄存器直接跟内存交换数据,所以除了寄存器,还必须了解内存怎么存储数据。
程序运行的时候,操作系统会给它分配一块内存,用来存储程序和运行产生的数据,比如从 0x1000
到 0x8000
,起始地址是较小的那个地址,结束地址是较大的那个地址。
程序运行过程中,对于动态的内存占用请求(比如新建对象),系统就会从预先分配好的那段内存中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址 0x1000
开始给他分配,一直分配到 0x100A
,如果在要求得到 22
个字节,那么就分配到 0x1020
。
这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)
。它由起始位置开始,从低位(地址)向高位(地址)增长。 Heap
的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。
动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap
中的数据,需要一个保存在stack
中的指针,因为要通过stack
中的指针访问heap
中的数据。
可以认为stack
中的一个指针仅仅是一个整型变量,保存了heap
中特定内存地址的数据。简而言之,操作系统使用stack
段中的指针值访问heap
段中的对象。如果stack
对象的指针没有了,则heap
中的对象就不能访问。这也是内存泄露的原因。
2.2、 栈 Stack
除了 Heap
外,其他的内存占用叫做 Stack(栈)
。简单来说,Stack
是由于函数运行而临时占用的内存区域。
int main() {
int a = 2;
int b = 3;
}
上面代码中,系统开始执行 main
函数时,会为它在内存里面建立一个 帧(frame)
,所有的 main
的内部变量(比如 a
和 b
)都保存在这个 帧
里面。main
函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。
如果函数内部调用其他函数,会发生什么情况呢?
int main() { int a = 2; int b = 3; return add_a_and_b(a,b); }
上面代码中,main
函数调用了 add_a_and_b
函数。执行到这一行的时候,系统也会为 add_a_and_b
新建一个 帧(frame)
,用来存储它的内部变量。也就是说,此时同时存在两个帧:main
和 add_a_and_b
。一般来说,调用栈有多少层,就有多少帧。
等到 add_a_and_b
运行结束,它的帧就会被回收,系统会回到刚才 main
函数中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。
所有的帧都存放在 Stack
,由于帧是一层层叠加的,所以 Stack
叫做 栈。生成新的帧,叫 入栈
,英文单词是 push
;栈的回收叫 出栈
,英文是 pop
。Stack
的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束执行),这种叫做 后进先出 的数据结构。每一次函数执行结束,就自动释放一个帧,所有的函数执行结束,整个 Stack
就都释放了。
Stack
是由内存区域的结束地址开始,从高位(地址)向地位(地址)分配。比如,内存区域的结束地址是 0x8000
,第一帧假定是 16
字节,那么下一次分配的地址就会从 0x7FF0
开始;第二帧假定需要 64
字节,那么地址就会移到 0x7FB0
。
三、Stack
段和Heap
的区别
stack
对象的优点:
- 创建速度快
- 管理简单,它有严格的生命周期
stack
对象的缺点:
不灵活,创建时长度是多大就一直是多大,创建时是哪个函数创建的,它的owner 就一直是它。不像
heap
对象那样有多个owner
,其实多个owner
等同于引用计数。只有heap
对象才是采用“引用计数”方法管理它。
堆空间和栈空间的大小是可变的,堆空间从下往上生长,栈空间从上往下生长。
Stack 对象的创建
只要栈的剩余空间大于 Stack
对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出。
Heap 对象的创建
操作系统对于内存Heap
段是采用链表进行管理的。操作系统有一个记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找第一个空间大于所申请的heap
节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。
例如:
NSString
的对象就是stack
中的对象,NSMutableString
的对象就是heap
中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner
, 适用于引用计数管理内存管理模式。两类对象的创建方法也不同,前者直接创建
NSString * str1=@"welcome";
,而后者需要先分配再初始化NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];
。
下面就NSString和NSMutableString进行实例演练:
1.copy和mutableCopy
NSString *str1= @"a"; // 常量字符串存放在常量区
NSString *str2= [str1 copy];
NSString *str3 = [str1 mutableCopy];
NSLog(@"str1 地址= %p", str1);
NSLog(@"str2 地址= %p", str2);
NSLog(@"str3 地址= %p", str3);
2022-08-31 10:18:30.599890+0800 ceshi[68188:2659402] str1 地址= 0x100004018
2022-08-31 10:18:30.600483+0800 ceshi[68188:2659402] str2 地址= 0x100004018
2022-08-31 10:18:30.600544+0800 ceshi[68188:2659402] str3 地址= 0x10307beb0
可以很明显的看出来,我们进行的copy操作为浅拷贝,两个指针(str1和str2)指向的是同一块内存地址,
我们进行的mutableCopy操作为深拷贝,指向的是两块存储相同内容的内存地址.原因是mutableCopy操作是将其从栈拷贝到堆上引用计数加1
2、NSString
NSString *str1= @"123";
NSString *str2= @"123";
NSLog(@"str1 address= %p", str1);
NSLog(@"str2 address= %p", str2);
if (str1 == str2) {
NSLog(@"str1 == str2");
str1 = @"456";
NSLog(@"str1 address= %p",str1);
NSLog(@"str2 address= %p",str2);
str1 = nil;
NSLog(@"str2 address= %p",str2);
}
2022-08-31 10:23:16.105566+0800 ceshi[68242:2662644] str1 address= 0x100004010
2022-08-31 10:23:16.106070+0800 ceshi[68242:2662644] str2 address= 0x100004010
2022-08-31 10:23:16.106148+0800 ceshi[68242:2662644] str1 == str2
2022-08-31 10:23:16.106216+0800 ceshi[68242:2662644] str1 address= 0x100004090
2022-08-31 10:23:16.106257+0800 ceshi[68242:2662644] str2 address= 0x100004010
2022-08-31 10:23:16.106293+0800 ceshi[68242:2662644] str2 address= 0x100004010
指针str1和指针str2指向同一内存地址
当str1改变内容后,创建了新的对象,则str1指向另一块内存地址,所以将str1置为nil,完全不影响str2
3、NSMutableString
NSMutableString *mutableStr1 = [[NSMutableString alloc]initWithString:@"123"];
NSMutableString *mutableStr2 = [[NSMutableString alloc]initWithString:@"123"];
NSLog(@"mutableStr1 address= %p",mutableStr1);
NSLog(@"mutableStr2 address= %p",mutableStr2);
if (mutableStr1 == mutableStr2) {
NSLog(@"mutableStr1 == mutableStr1");
} else {
NSLog(@"mutableStr1 != mutableStr1");
[mutableStr1 setString:@"456"];
NSLog(@"mutableStr1 address= %p",mutableStr1);
NSLog(@"mutableStr2 address= %p",mutableStr2);
}
2022-08-31 10:30:32.323975+0800 ceshi[68323:2666192] mutableStr1 address= 0x10076aa80
2022-08-31 10:30:32.324494+0800 ceshi[68323:2666192] mutableStr2 address= 0x10076aaf0
2022-08-31 10:30:32.324557+0800 ceshi[68323:2666192] mutableStr1 != mutableStr1
2022-08-31 10:30:32.324606+0800 ceshi[68323:2666192] mutableStr1 address= 0x10076aa80
2022-08-31 10:30:32.324642+0800 ceshi[68323:2666192] mutableStr2 address= 0x10076aaf0
从上图我们可以看到
mutableStr1和mutableStr2虽然内容相同,但指向的是不同的内存地址,改变内容后仍然指向自己的内存地址,所以,NSMutableString为深拷贝
关于NSString存放堆栈的探究:
NSString *str1 = [NSString stringWithFormat:@"小强"];
NSString *str2 = [NSString stringWithFormat:@"1"];
NSString *str3 = [NSString stringWithFormat:@"a"];
NSString *str4 = @"123456789";
-
__NSCFConstantString
显然是常量字符串,自然就是存储在常量区。 -
__NSCFString
表示为oc对象,NSString
就是封装的CFString
,0x6000000315c0
地址显示这个字符串对象存储在堆中。 -
NSTaggedPointerString
这个类表示这是字符串的一种指针Tagged Pointer,0xa636261646362617
这个地址为什么如此与众不同呢,接下来我们就简单介绍这钟字符串的存储指针。
NSMutableString *str1= [NSMutableString stringWithFormat:@"123"];
NSMutableString *str2 = str1;
NSLog(@"str1 = %@",str1);
NSLog(@"str2 = %@",str2);
[str1 setString:@"456"];
NSLog(@"str1 = %@",str1);
NSLog(@"str2 = %@",str2);
2022-08-31 11:47:05.833767+0800 ceshi[69124:2702123] str1 = 123
2022-08-31 11:47:05.834591+0800 ceshi[69124:2702123] str2 = 123
2022-08-31 11:47:05.834653+0800 ceshi[69124:2702123] str1 = 456
2022-08-31 11:47:05.834692+0800 ceshi[69124:2702123] str2 = 456
Program ended with exit code: 0