该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
上一节介绍了消息传递和消息转发,接下来我们看看内存管理相关的内容;
第4章 内存管理
恰当的内存管理是正确而高效地开发程序的关键;
本章详细介绍为OC程序分配和释放内存的途径、OC的内存模型,以及如何编写实现恰当内存管理的程序;
计算机操作系统为程序分配有限内存,程序应该仅适用其必需的内存;
OC语言及其运行时环境提供了支持应用内存管理的机制;
4.1 程序的内存使用情况
计算机内存中存储和使用程序的方式:
OC可执行程序是由(可执行)代码、初始化和未初始化的程序数据、链接信息、重定位信息、局部数据和动态数据构成的;
其中:
程序数据包括静态方式声明的变量和程序常量(即在程序编译时在代码中设置的常数);
可执行代码、程序数据以及链接与重定位信息会以静态方式被分配内存,并在程序的声明周期中一直存在;
局部(自动)数据在语句中声明并且只在该语句中有效,语句执行后,局部数据不会继续存在;
从语法上来说,OC的复合语句块就是又括号{}封装的语句集合;
自动数据被存储在程序栈中,程序栈通常是在执行程序/线程前就被设定尺寸的内存段;
栈用于存储局部变量和调用方法/函数的上下文数据;
上下文数据包括方法的输入参数、返回值,以及调用完方法后继续执行程序的代码地址;
操作系统会自动管理这些内存;这些数据会获得栈中的内存,而分配给这些数据的内存会在他们失效后被释放;
在运行时,OC会将创建的对象(通过NSObject的alloc方法创建的)存储在动态分配的内存即堆内存中;
以动态方式创建对象就意味着需要进行内存管理,因为在堆内存中创建的对象永远不会超过其作用范围;
地位地址----------------------->高位地址
可执行的二进制代码->程序数据->堆->未使用的内存->栈->输入程序的数据
为程序代码分配的内存是在编写程序的时候设置的,因此占用的是系统内存;
OC程序内存管理的必要性:
一方面,程序的栈尺寸(通常)是在程序启动时确定的,会自动由系统管理;
另一方面,在OC中对象是在程序执行时动态创建的,不会有系统自动回收;
不进行内存管理和错误的内存管理会导致:
1)内存泄漏:
程序没有释放不再使用的对象,就会导致该问题;继续分配内存的话,最终会耗尽系统内存;
2)悬挂指针:
程序释放了仍在使用的对象,会导致该问题;如果将来程序尝试访问这些对象,就会出现程序错误;
4.2 OC的内存模型
OC的内存管理是通过引用计数实现的;
引用计数是一种通过对象的唯一引用,确定对象是否正在被使用的技术;
如果对象的引用计数降到了0,对象就会被视为不再有用,而且运行时系统也会释放它的内存;
苹果OC开发环境提供了两种内存管理机制:
手动管理(MRR)(也就是我们通常说的MRC,C是count的意思,是相对ARC来说的);
自动引用计数(ARC);
4.3 手动管理
手动管理(MRR):是一种建立在对象所有权概念上的内存管理机制;
只要对象所有者还存在,对象就不会被OC运行时环境释放;
可以通过编写代码精确的管理对象的回收利用;
我们先来理解下访问和使用对象的方式,以及访问对象与对象所有权之间的差别;
4.3.1 对象引用和对象所有权
OC对象是通过指向OC对象内存地址的变量,以间接方式访问的;
这种变量就是C语言中的指针;
指针的应用范围很广,包括OC的基本数据类型与C语言的数据类型;
但,对象指针专门用于OC对象的交互操作;
对象指针实现了OC对象的访问功能;
但是,它们本身并不能管理所有权;
比如:
A * a = [[A alloc] init];
A * b = a;
声明一个对象指针 ,指向另一个同类型的对象指针;我们虽然改变了指针的指向,但是并未设置原始对象的所有权;
这会导致,如果a被释放了,b就指向了一个不合法的对象;
要以MRR方式管理对象生命周期:即保留和释放,在编写代码时需要遵守一系列内存管理规则;
4.3.2 内存管理基本原则
要正确使用MRR,编写代码时必须在获取对象所有权与释放对象所有权之间进行平衡;
因此需遵循:
1)为创建的所有对象设置所有权:
应使用名称以alloc、new、copy或mutableCopy开头的方法创建OC对象;
还应通过向块发送copy消息,以动态的方式创建OC块对象;
2)应使用retain方法获取对象(你尚未拥有)的所有权:
使用NSObject的retain方法可以获得对象的所有权;
使用retain可以获取你想要长时间使用的对象的所有权,这样做通常可以将其存储为属性值,并防止其他操作无意中释放该对象;
3)当不再使用某个对象时,必须放弃其所有权:
使用NSObject类的release和autorelease方法可以释放对象的所有权;
使用autorelease方法可以在当前自动释放代码块的末尾,放弃对象的所有权;
如果对象的引用计数为0,这两种方法都会对对象执行dealloc方法;
4)不能放弃不归你所有的对象的所有权:
这样做会导致过早滴释放这些对象,如果程序尝试方位已经被释放的对象,就会出错;
注意,所有权是对象指针变量相对于对象来讲的,对象一直都在,获取和释放的是对象指针变量对 对象的所有权;对象的所有权(用计数表示)清0时,相应的内存空间也会被运行时系统释放;
释放操作:
1)释放内存:
当对象的引用计数为0时,运行时系统会通过NSObject类的dealloc方法释放掉该对象使用的内存;(注意理解这句话)
该方法还提供了放弃子类对象所有权的框架:
-(void)dealloc{
[... release];
...
[super dealloc];
}
你编写的类(通常都是NSObject类的子类)都应该重写dealloc方法,调用它们实例变量的release方法,然后调用它们父类的dealloc方法;
通过这种方式可以是你编写的类,遵循类的层次结构以适当的方式放弃对象的所有权;
2)通过autorelease方法延迟释放操作:
通过NSObject类的autorelease方法可以在自动释放池代码块的末尾,调用对象中的方法;
自动释放代码块 提供了在将来某个时间放弃对象所有权的机制,因而无须编写调用对象中release方法的具体代码,并能避免对象立刻被释放的情况;
使用@autorelease指令可以定义自动释放池代码块:
@autorelease{
//创建自动释放对象的代码
...
}
应该总是将创建自动释放对象的代码放在自动释放池代码块中;
否则他们将无法收到release消息,从而导致内存泄漏;
用于创建iOS和Mac OS X应用的苹果应用框架,尤其是AppKit和UIKit,能够自动提供自动释放代码块;
需要手动编写自动释放代码块的情况有以下几种:
(1)你编写的程序不是以苹果UI框架为基础的,如命令行工具;
(2)你实现的逻辑中含有创建很多临时对象的循环;
为了降低应用占用内存的最大值,你在该循环中添加自动释放池代码块,以便在下一次迭代前处置这些对象;
(3)你编写的应用派生出来一个或多个辅助线程;
你必须在执行辅助线程的位置添加自己编写的自动释放池代码块,否则你的应用就会内存泄漏;
举个例子:
你可以向这样写:
@autorelease{
A * a = [[[A alloc] init] autorelease];
...
}
注意:
上述对象在创建并初始化之后,会立即收到autorelease消息,这通常由一条复合语句实现;
这种设计可以确保所有通过autorelease消息创建的对象都会在程序结束前、在自动释放代码块的末尾被释放;
4.3.3 使用MRR
示例:
NSString * string1 = [NSString new];//new为创建的对象设置所有权
NSString * string2 = string1; //对象指针变量赋值
[string2 retain]; //对象指针变量使用retain获取对象所有权
AClass * a = [[AClass alloc] init];//alloc为创建的对象设置所有权
[string1 release];
[string2 release]; //对象指针变量使用release放弃对象所有权
[a release];
以上示例展示了 以动态方式创建对象时,调动初始化和释放方式的顺序;
所有对象的创建/保留和释放消息都达到了平衡;
Xcode的Product菜单的Analyze选项:该工具会分析程序,检测潜在的内存泄漏,悬挂指针等问题;(Commend + shift + B)
当然也可以用Xcode Instrument工具进行内存使用情况的分析;
下一节介绍自动引用计数的使用;