内存区域
heap 和 stack 是内存管理的两个概念。这里指的不是数据结构上面的堆与栈,这里指的是内存的分配的两个区域:堆区和栈区。(不过确实是有相似之处)。
iOS 中某个 app 使用的内存不是一段连续的统一分配空间,而是分布在不同的内存区域,如下:
栈区(stack):一个线程会分配一个 stack,当一个函数被调用时,例如 app 最开始运行函数 main() ,一个 stack frame (栈帧)就会被压到 stack 里。里面包含这个函数涉及的参数,局部变量,返回地址等相关信息。当执行第二个函数时,又一个 stack frame 会被压到 stack 里。当函数返回后,这个栈帧就会被销毁。而且类似数据结构中的 stack,栈区是一种后进先出(LIFO )结构 。这一切都是自动的,我们不需要管理栈区变量的内存;栈区地址从高到低分配。
堆区(heap):我们使用 alloc/new/copy/mutableCopy 创建一个对象时,堆区就会分配一段内存,这部分内存需要我们进行管理,简单来讲 ARC 时代就是通过我们主动声明对象创建的内存管理语义(strong,weak,copy,assign 等),然后编译器在编译的时候自动添加retain、release、autorelease 等方法,这些方法会通过一种叫作“引用计数”的方式进行内存管理,具体我们后面再讲。堆区的地址是从低到高分配。
全局区/静态区(static):包括两个部分:全局未初始化区(BSS区) 、全局初始化区(数据区)。也就是说,全局区/静态区 在内存中是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;eg:int a;未初始化的。int a = 10;已初始化的。
常量区:常量字符串就是放在这里。
代码区:存放App二进制代码,代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
每个 Objective-C 对象都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“*”字符:
NSString *pointerVariable = @"someString";
pointerVariable作为一个局部变量,它是栈上的一个指针变量,@"someString" 是堆上的内存对象,pointerVariable 变量内存放着堆上对象的内存地址。
为什么是 Stack 和 Heap?
首先所有的Objective-C对象都是分配在heap的。在OC最典型的内存分配与初始化就是这样的。
NSObject *obj = [[NSObject alloc] init];
一个对象在alloc的时候,就在 Heap 分配了内存空间。
stack对象通常有速度的优势,而且不会发生内存泄露问题。那么为什么OC的对象都是分配在 heap 而不是 stack 呢?
原因在于:
- stack 对象的生命周期所导致的问题。例如一旦函数返回,则所在的stack frame就会被摧毁。那么此时返回的对象也会一并摧毁。这个时候我们去retain这个对象是无效的。因为整个stack frame都已经被摧毁了。简单而言,就是stack对象的生命周期不适合Objective-C的引用计数内存管理方法。
- stack对象不够灵活(LIFO),不具备足够的扩展性。创建时长度已经是固定的,而stack对象的拥有者也就是所在的 stack frame.
stack 和 heap 的工作原理
栈区(stack):栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
堆区(heap):系统使用一个链表来维护所有已经分配的内存空间,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。