【Effective Objective-C】内存管理

前言

在OC这种面相对象的语言里,内存管理是个重要概念。要想用一门语言写出内存使用效率高而且又没有bug代码,就得掌握其内存管理模型的种种细节。
一旦理解了这些规则,你就会发现,其实OC的内存管理没那么复杂,而且有了“自动引用计数”(Automatic Reference Counting,ARC)之后,就变得更为简单了。ARC几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。

理解引用计数

OC语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完之后,就递减其计数。计数变为0,就表示没人关注此对象了,于是,就可以把它销毁。要想写出优秀的OC代码,必须完全理解此问题才行,即便打算用ARC来编码也是如此。
从Mac OS X 10.8开始,“垃圾收集器”(garbage collector)已经正式废弃了,以OC代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集。因此,掌握引用计数机制对于学好OC来说十分重要。Mac OS X程序已经不能再依赖垃圾收集器了,而iOS系统不支持此功能,将来也不会支持。
已经用过ARC的人可能会知道:所有与引用计数有关的方法都无法编译,然而现在先暂时忘掉这件事。那些方法确实无法用在ARC中,不过本条就是要从OC的角度讲解引用计数,而ARC实际上也是一种引用计数机制,所以,还是要谈谈这些在开启ARC功能时不能直接调用的方法。

引用计数工作原理

在引用计数的架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在OC中叫做“保留计数”(retain count),不过也可以叫做“引用计数”(reference count)。NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:

  1. retain递增保留计数
  2. release递减保留计数
  3. autorelease待稍后清理“自动释放池”时,再递减保留计数。
  • 查看保留计数的方法叫retainCount,此方法不太有用,即便在调试时也如此,所以并不推荐大家使用这个方法。
  • 对象创建出来时,其保留计数至少为1.若想令其继续存活,则调用retain方法。若是某部分代码不在使用此对象不想令其继续存活,那就调用releaseautorelease方法,最终当保留计数归零时,对象就回收了。也就是说,系统会将其占用的内存标记为“可重用”。此时,所有指向该对象的引用也都变得无效了。

如图演示了对象自创造出来之后历经一次“保留”及两次“释放”操作的过程。

请添加图片描述

  • 应用程序在其生命期中会创建很多对象,这些对象都相互联系着。

在下方图中,ObjectBObjectC都引用了ObjectA。若ObjectBObjectC都不再使用ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectBObjectC继续存活,而应用程序里又又另外一些对象想令那些对象继续存活。如果按“引用树”回溯,那么最终会发现一个“根对象”。在Mac OS X应用程序中,此对象就是NSApplication对象;而在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时所创建的单例。

下面这段代码有助于理解这些方法的用法:

NSMutableArray *array = [[NSMutableArray alloc] init];

NSNumber *number = [[NSNumber alloc] initWithInt:1337];

[array addObject:number];

[number release];
//do something with 'array'
[array release];

请添加图片描述

为避免在不经意间使用了无效对象,一般调用完 release 之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为"悬挂指针(dangling pointer)"。比方说,可以这样编写代码来防止此情况发生∶

NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;

属性存取方法中的内存管理

如前所述,对象图由互相关联的对象所构成。刚才那个例子中的数组通过在其元素上调用 retain 方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问"属性"(参见第6条)来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为" strong关系"(strong relationship).则设置的属性值会保留。比方说.有个名叫 foo 的属性由名为_foo 的实例变量所实现,那么,该属性的设置方法会是这样∶

- (void) setFoo:(id) foo [
	[foo retain];
	[_foo release];
	_foo = foo;
}

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么,先执行的 release 操作就可能导致系统将此对象永久回收。而后续的 retain 操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针

自动释放池

Objective-C的引用计数架构中,自动释放池是一项重要特性。调用release 会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次"事件循环"(event loop)时递减,不过也可能执行得更早些。

此特性很有用,尤其是在方法中返回对象时更应该用它。在这种情况下,我们并不总是想令方法调用者手工保留其值。比方说,有下面这个方法∶

- (NSString *)stringValue {
   
	NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
	return str;
}

此时返回的 str 对象其保留计数比期望值要多1+1 retain count)、因为调用 alloc 会令保留计数加1,而又没有与之对应的释放操作。保留计数多1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身就一定是1,它可能大于1,不过那取决于"initWithFormat∶"方法内的实现细节。你要考虑的是如何将多出来的这一次保留操作抵消掉。
但是,不能在方法内释放 str,否则还没等方法返回,系统就把该对象回收了。这里应该用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越"方法调用边界"(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池(参见第 34条)时执行,除非你有自己的自动释放池。否则这个时机指的就是当前线程的下一次事件循环。改写stringValue 方法,使用autorelease 来释放对象∶

- (NSString *)stringValue {
   
	NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
	return [str autorelease];
}

修改之后,stringValue 方法把NSString 对象返回给调用者时,此对象必然存活。所以我们能够像下面这样使用它∶

NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);

由于返回的 str 对象将于稍后自动释放,所以多出来的那一次保留操作到时自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以NSLog 语句在使用str 对象前不需要手工执行保留操作。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留,并于稍后释放∶

_instanceVariable = [[self stringValue] retain];
//...
[_instanceVariable release];
  • 由此可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

保留环

使用引用计数机制时,经常要注意的一个问题就是“保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄漏,因为循环中的对象其保留计数不会降为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。下图里的每个对象都引用了另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是1。
在垃圾收集环境中,通常将这种情况认定为"孤岛"(island of isolation)。此时,垃圾收集器会把三个对象全都回收走。而在 Objective-C的引用计数架构中,则享受不到这一便利。通常采用"弱引用"(weak reference,参见第33条)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄漏。

请添加图片描述

要点

  1. 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
  2. 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

以ARC简化引用计数

引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。所以Clang 编译器项目带有一个"静态分析器"(static analyzer),用于指明程序里引用计数出问题的地方。举个例子,假设下面这段代码采用手工方式管理引用计数∶

if ([self shouldLogMessage]) {
   
	NSString *message = [[NSString alloc] initWit
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值