我们要想了解内存管理的知识前,必须先搞明白计算机的内存分配以及计算机是如何处理内存的。
内存指的就是RAM(random access memory),内存分配区域主要分为五个区:栈区(系统管理的地方)、堆区(程序员控制的地方)、静态区(全局区)、常量区、代码区。
栈区(stack)由编译器自动分配并释放
,存放的是函数的参数值
,局部变量
,基本类型的变量和对象引用类型
,方法调用的实参也是保存在栈区
。所以我们可以把栈看做一个临时寄存
、交换的内存区
。
栈是系统数据结构,对应线程/进程是唯一的
。优点是快速高效,缺点是有限制,数据操作不灵活。
由程序员分配(malloc、new)和释放(delete),主要存在new构造的对象和数组如果不释放,可能造成内存泄露,程序结束时可能会由操作系统回收。
堆向高地址扩展的数据结构,是不连续的内存区域
。 程序员负责在何时释放内存,在ARC程序中,计数器为0的时候,在当次的runloop结束后,释放掉内存。堆中的所有东西都是匿名的,这样不能按名字访问,而只能通过指针访问
。
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续性,从而造成大量的碎片 ,使程序效率降低。
文字常量区存放常量字符,程序结束后自动释放,不允许修改。
存放函数体的二进制代码
全局变量和静态变量都被分配到同一块内存。
全局区(静态区) (static): 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后由系统释放。
注意:全局区又可分为未初始化全局区(BSS段)和初始化全局区(DATA段)。
在学习MRC之前除了要了解计算机关于内存的分配区域之外,还需要了解一些知识,具体如下:
另外现在我们的XCode默认为ARC环境,如果需要MRC我们需要手动设置:
我们要想手动进行内存管理就需要知道哪些情况会引起计数的变化:
对象被废弃对象所占内存解除分配 并放回“可用内存池中”。
自己生成的对象,自己持有
/*
* 自己生成并持有该对象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
非自己生成的对象,自己也能持有。
/*
* 持有非自己生成的对象
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象
不在需要自己持有对象的时候,释放。
/*
* 不在需要自己持有的对象的时候,释放
*/
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象
/*
* 指向对象的指针仍就被保留在obj这个变量中
* 但对象已经释放,不可访问
*/
非自己持有的对象无需释放。
/*
* 非自己持有的对象无法释放
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为
这句话听着很绕口,但其实很好理解,但是要了解这句话首先我们要知道一个叫autorelease
的东西。
所谓autorelease
就是自动释放,注意:autorelease
和ARC 是完全不同的两个东西,没有任何联系。
类似于C语言的局部变量,超出变量作用域,局部变量就会被废弃,不可再访问。
autorelease会在超出作用域时,调用对象的release实例方法,与C语言不同的是,编程人员可以设定变量的作用域,类似下图:
我们来看一下代码:
- (id) getAObjNotRetain {
id obj = [[NSObject alloc] init]; // 自己持有对象
[obj autorelease]; // 取得的对象存在,但自己不持有该对象
return obj;
}
取得的对象存在,但自己不持有该对象,那么该对象被谁持有了呢?
调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中)
,调用方拿到了对象,但这个对象还不被调用方所持有。
那么问题来了,autorelease和release有什么区别?
Autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该对象放入了当前的Autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用Release。
autorelease pool的作用就是来避免频繁申请/释放内存(就是pool的作用了)。
像 [NSMutableArray array]
、[NSArray array]
都可以取得谁都不持有的对象,这些方法都是通过 autorelease 实现的。
autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理。等到自动释放池销毁时,所有对象统一都会被释放。
看一下下面的代码:
int main(int argc, const char * argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id obj = [[NSObject alloc]init];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[obj autorelease];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[pool drain];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
return 0;
}
首先生成对象引用计数+1,接着将持有权转移给了自动释放池引用计数不变,然后销毁池子,池子中的所用对象调用release,引用计数-1,变为0。
假如我将代码改成如下:
int main(int argc, const char * argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id obj = [[NSObject alloc]init];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[obj autorelease];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[pool drain];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[obj autorelease];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
return 0;
}
这样会发生什么呢?这样的话程序会崩溃,因为池子销毁的时候,池子里的所有对象 都被释放了,调用一个不存在的对象,程序当然会崩溃。
NSObject 协议中定义的内存管理方法与遵守这些方法命名约定的自定义方法的组合提供了用于引用计数环境中的内存管理的基本模型。NSObject 类还定义了一个dealloc 方法,该方法在对象被销毁时自动调用,也就是在引用计数为0时。
看一段代码:
NSNumber *number = [NSNumber numberWithInt:1];
NSLog(@"retainCount = %lu",[number retainCount]);
我们看一下打印结果:
结果大的离谱啊,查阅了一下官方的文档,第一句就是“Do not use this method.”,后面给出了说明,因为Autorelease pool的存在,对于内存的管理会相当复杂,retainCount就不能用作调试内存时的依据了。