一、iOS 应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在几个不同的内存区域 

ios内存数据结构 ios内存分配深度解析_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 还要指挥寄存器直接跟内存交换数据,所以除了寄存器,还必须了解内存怎么存储数据。

程序运行的时候,操作系统会给它分配一块内存,用来存储程序和运行产生的数据,比如从 0x10000x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

程序运行过程中,对于动态的内存占用请求(比如新建对象),系统就会从预先分配好的那段内存中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址 0x1000 开始给他分配,一直分配到 0x100A,如果在要求得到 22 个字节,那么就分配到 0x1020

ios内存数据结构 ios内存分配深度解析_Stack_02

这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始位置开始,从低位(地址)向高位(地址)增长。 Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap 中的数据,需要一个保存在stack中的指针,因为要通过stack中的指针访问heap 中的数据。

可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。

 2.2、 栈 Stack

除了 Heap 外,其他的内存占用叫做 Stack(栈)。简单来说,Stack 是由于函数运行而临时占用的内存区域。

ios内存数据结构 ios内存分配深度解析_objective-c_03

int main() {
    int a = 2;
    int b = 3;
}

上面代码中,系统开始执行 main 函数时,会为它在内存里面建立一个 帧(frame),所有的 main 的内部变量(比如 ab)都保存在这个 里面。main 函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

ios内存数据结构 ios内存分配深度解析_Stack_04

如果函数内部调用其他函数,会发生什么情况呢?

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),用来存储它的内部变量。也就是说,此时同时存在两个帧:mainadd_a_and_b。一般来说,调用栈有多少层,就有多少帧。

ios内存数据结构 ios内存分配深度解析_ios_05

等到 add_a_and_b 运行结束,它的帧就会被回收,系统会回到刚才 main 函数中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。

所有的帧都存放在 Stack ,由于帧是一层层叠加的,所以 Stack 叫做 。生成新的帧,叫 入栈,英文单词是 push;栈的回收叫 出栈,英文是 popStack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束执行),这种叫做 后进先出 的数据结构。每一次函数执行结束,就自动释放一个帧,所有的函数执行结束,整个 Stack 就都释放了。

Stack 是由内存区域的结束地址开始,从高位(地址)向地位(地址)分配。比如,内存区域的结束地址是 0x8000,第一帧假定是 16 字节,那么下一次分配的地址就会从 0x7FF0 开始;第二帧假定需要 64 字节,那么地址就会移到 0x7FB0

ios内存数据结构 ios内存分配深度解析_ios_06

 三、Stack 段和Heap 的区别

stack 对象的优点:

  1. 创建速度快
  2. 管理简单,它有严格的生命周期

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";

ios内存数据结构 ios内存分配深度解析_ios_07

  • __NSCFConstantString显然是常量字符串,自然就是存储在常量区。
  • __NSCFString表示为oc对象,NSString就是封装的CFString0x6000000315c0地址显示这个字符串对象存储在堆中。
  • 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