1. 前言
本篇文章是对 Automatic reference counting(ARC) 的介绍的第一篇文章,主要介绍 Manual reference counting(MRC)和ARC之间区别和联系。注意的是本文提到的消息和方法为同样的意思,可以理解为函数调用。
2. 内存分布
一个程序有自己的内存空间,也即虚拟地址空间。一个程序的数据和对象都存在于该虚拟地址空间中。但是该虚拟地址空间并不是无限大的,这就意味着并不能无限生成对象,也就是资源有限。那么在该虚拟内存中主要有两部分存储对象:栈和堆。其中栈内存是由编译器自动管理,而堆内存需要程序员手动管理。如果程序对堆内存使用不当,那么就会造成内存泄露。
3. Objective-C中的引用计数
3.1 什么叫引用计数?
引用计数是一种内存管理技术,大概可概括成:当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。 从下图图片中显示,生成一个NSString对象(可能这样不太恰当,但是比较直观,此时只需将NSString看作一个普通对象),此时有一个NSString *指针引用它,它的引用计数为1。其中ptr1分配在栈内存上,而NSString对象分配在堆内存上。(此时无需考虑是MRC还是ARC,下面例子的代码只是为了方便介介绍引用计数的概念)
NSString *ptr1 = [NSString stringWithString:@"str"];
当我们写下如下代码,
NSString *ptr2 = ptr1;
此时引用计数的情况变成了如下图所示,从图片中可以看出,此时NSString对象有两个指针引用它,那么它的引用计数为2。
那么当执行如下代码,
ptr1 = nil;
ptr2 = nil;
此时引用计数的情况如下图所示,此时堆内存对象的引用计数为0,则对象的堆内存将会被系统回收。
3.2 Objective-C中的内存管理策略
3.2.1 基本的内存管理原则
内存管理模型基于对象所有权。一个堆对象可能有一个或多个所有者。只要一个对象至少有一个所有者持续存在。如果一个对象没有了所有者,即引用计数为0,runtime system将会自动销毁该对象。在编写代码的时候请想好两个问题,什么时候你拥有该对象的所有权,什么时候你失去该对象的所有权。CoCo设置了下面几个原则,其实也就是你在MRC环境下,自己应该遵循什么原则来编写代码。
-
你创建的对象,则你拥有该对象的所有权
比如使用alloc,new,copy和mutableCopy等方法创建的对象,表示你拥有所有权。当你在使用上面提到的方法创建的对象,你知道此时你拥有该对象的所有权,那么当你不再使用该对象时,你应该释放所有权。也就是使用release。
{
NSSting *str = [NSString alloc] init];
//do something
[str release];
}
-
你可以使用retain来获取某个对象的所有权。
某个方法返回的对象能保证在该方法中,该对象有效。并且能够安全的将对象返回给调用者。你可以在两种情况下使用retain:(1)在访问器或init方法中,当你存储属性值,该属性指向一个堆对象,可以获得该对象的所有权(2)防止其它操作的导致对象被销毁。
-
当你不在需要拥有所有权的对象时,可以放弃所有权。
通过release或autorelease来释放对象的所有权。
-
你不准对一个没有所有权的对象,进行释放所有权操作。
获取所有权可以对对象发送`retain`消息,而释放所有权则可以发送’release‘或`autorelease`(延迟release)消息。注意的是release消息并不是销毁对象,只是将对象的引用计数减1。对象的内存释放需要等其引用计数为0。其中retain和release消息介绍如下:
- retain taking no arguments and returning a pointer to the object.
- release, taking no arguments and returning
void. 当显式的发送retain,release和autorelease消息的代码均为非ARC环境。因为在ARC环境下,不允许显式的对对象发送这些消息。
3.2.2 一个例子
本小节主要介绍在MRC的情况下,程序员如何编写管理对象生命周期的代码。
{
Person *aPerson = [[Person alloc] init];
//...
NSString *name = aPerson.fullName;
//...
[aPerson release];
}
根据命名规则,当程序员创建Person对象,而Person对象创建自alloc,所以它是自己创建并拥有所有权,并且将指针值赋值给aPerson。所以当不需要它时,需要对其发送release消息(MRC)。因为person’s name不是从owning methods(指的是对其发送了retain的方法)中返回,所以不要对其发送release消息。
3.2.3 使用autorelease来发送一个延迟的release消息
当你需要返回一个对象,该对象从别的方法生成,则此时使用autorelease。如下情况所示:
-(NSString *)fullName{
NSString *str = [NSString alloc] initWithFormat:"%@ %@", self.firstName, self.lastName] autorelease];
return str;
}
你拥有指针str所指向的对象,因为它由alloc生成。但是你必须释放对它的所有权,当str销毁时,也即该函数执行结束。如果使用release,那么该对象将会被销毁在它调用之前,那么该方法将会返回一个无效的对象。此时应该使用autorelease,来延缓该对象的释放。当你调用该方法时,可以获得一个有效的对象,但是并不拥有其所有权。所以需要发送retain消息。
{
//此时,fullName并不拥有该对象的所有权
NSString *fullName = [Person fullName];
//执行retain,拥有该对象的所有权。
[fullName reatin];
//...
//释放对该对象的所有权
[fullName release];
}
你也可以将fullName的代码写成如下:
- (NSString *)fullName{
NSString *str = [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
return str;
}
因为stringWithFormat方法并不符合alloc,new,copy和mutableCopy等命名约定,所以它返回的对象指针并不持有该对象。那么当你掉用该fullName方法,需要对其进行retain操作,不再使用时,执行release操作。下面的写法就是错误的。
-(NSString *)fullName{
//1. 返回获得所有权的对象指针,此时该对象的引用计数为1。
NSString *str = [NSString alloc] initWithFormat:"%@ %@", self.firstName, self.lastName];
return str;
}
根据命名的约定,fullName的调用者根据名字,会觉得对返回的字符串没有拥有所有权,因此调用者可能写下下面的代码。
{
NSString *name = [aPerson fullName];
//2. 根据名字约定,要对其retain操作,获得该对象的所有权(这是调用者主观认为的,实际上返回的指针已经获得对象的所有权),该对象的引用计数为2。
[name retain];
//... do something
//3. 执行release操作,将对象的引用计数减去1。此时该对象的引用计数为1。
[name release];
}
从上面的例子可以看出,fullName中生成的对象最终的引用计数为1,这就造成了内存泄漏。所以在MRC环境下,必须遵循命名约定来实现方法,否则可能会造成内存泄露。
3.2.4 执行dealloc来放弃对象的所有权
如果定义一个类,该类有一个或多个属性。那么需要在dealloc方法中对这些属性执行release操作,释放其引用的对象的所有权。NSObject类中定义了一个方法dealloc,当一个对象没有所有者时,该方法会自动调用。dealloc的作用是释放对象自己所占的内存,并且释放其持有的资源,包括任何对象的实例变量的所有权。
@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
@implement Person
//...
- (void)dealloc{
[_firstName release];
[_lastName release];
[super dealloc];
}
需要注意的是永远不要直接调用dealloc,在子类的dealloc中的最后必须调用父类的dealloc方法,
当应用程序终止时,可能不会向对象发送dealloc消息。 因为进程的内存会在退出时自动清除,所以简单地让操作系统清理资源比调用所有内存管理方法更有效。
4. ARC
在编译其中开启ARC,项目的build setting中找到Objective-C Automatic Reference Counting可以开启ARC,当然现在的Xcode版本都是默认开启ARC。 ARC仍然遵循MRC的内存管理的几个基本原则,只是开启了ARC之后,就无需程序员手动编写retian等引用计数相关的代码。
这里与上文一个手动引用计数的例子进行比较。 对于fullName方法的调用
-(NSString *)fullName{
NSString *str = [NSString alloc] initWithFormat:"%@ %@", self.firstName, self.lastName] autorelease];
return str;
}
在MRC情况下:
{
NSString *name = [aPerson fullName];
//.... do something
//手动编写
[name release];
}
在ARC情况下:
{
NSString *name = [aPerson fullName];
//.... do something
//无需手动编写,release方法的调用由编译器自动完成。
//[name release];
}
5 总结
- 资源有限,所以需要内存管理
- Objective-C对象使用基于引用计数管理的方法
- 内存对象管理基于四个原则
- MRC需要手动编写retain,release和autorelase方法
- 编译器开启ARC后,程序员无需手动编写retain,release和autorelease方法,这一切由编译器完成。