Objective-C 回顾【三】之 内存管理

理解引用计数


Objective-C 语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其计数。计数变为 O,就表示没人关注此对象了,于是,就可以把它销毁。

引用计数工作原理

Objective-C 中,调用 alloc 方法所返回的对象由调用者所拥有。也就是说,调用者已通过 alloc 方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是 1。在 alloc 或。initWithInt: 方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于 1。能够肯定的是:保留计数至少为 1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了该计数还是递减了该计数。

1
2
3
4
5
6
7
8
9
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];

[array addObject:number];
[number release];

// do something with 'array'

[array release];

如上面的代码,创建完数组后,把 number 对象加人其中。调用数组的 addobject: 方法时,数组也会在 number 上调用 retain 方法,以期继续保留此对象。这时,保留计数至少为 2。接下来,代码不再需要 number 对象了,于是将其释放。现在的保留计数至少为 1。这样就不能照常使用 number 变量了。调用 release 之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道 number 对象在调用了 release 之后仍然存活。因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码

1
2
3
4
5
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];

NSLog (@"number = %@", number);

即便上述代码在本例中可以正常执行,也仍然不是个好办法。如果调用 release 之后,基于某些原因,其保留计数降至 O,那么 number 对象所占内存也许会回收,这样的话,再调用 NSLog 可能就将使程序崩溃了。笔者在这里只说“可能”,而没说“一定”,因为对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaiable pool)。如果执行 NSLog 时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见:因过早释放对象而导致的 bug 很难调试。

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

1
2
3
4
5
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];

number = nil;

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

1
2
3
4
5
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}

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

自动释放池

autorelease 此方法会在稍后递减计数,通常是在下一次“事件循环”时递减,不过也可能执行得更早些。

此特性很有用,尤其是在方法中返回对象时更应该用它。比如:

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

注意,这里不能在方法内释放 str 否则还没等方法返回,系统就把该对象回收了。这里应该调用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越“方法调用边界”后一定存活。实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。改写 stringValue 方法,使用 autorelease 来释放对象:

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

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

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

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

1
2
_instanceVariable = [[self stringValue] retain];
[_instanceVariable release];

由此可见,autorelease 能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

要点:

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

以 ARC 简化引用计数


变量的内存管理语义

  • __strong:默认语义,保留此值。
  • __unsafe_unretained:不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
  • __weak:不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
  • __autoreleasing:把对象“按引用传递”给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。

我们经常会给局部变量加上修饰符,用以打破由“块”所引入的“引用循环”。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致“引用循环”。可以用 __weak 局部变量来打破这种“引用循环”。

要点:

  • 有 ARC 之后,程序员就无须担心内存管理问题了。使用 ARC 来编程,可以省去类中的许多“样板代码”。
  • ARC 管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在 ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
  • 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC 将此确定为开发者必须遵守的规则。
  • ARC 只负责管理 Objective-C 对象的内存。尤其要注意:CoreFoundation 对象不归 ARC 管理,开发者必须适时调用 CFRetain/CFRelease。

在 dealloc 方法中只释放引用并解除监听


要点:

  • dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或 NSNotificationCenter 等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用 close 方法。
  • 执行异步任务的方法不应在 dealloc 里调用;只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已处于正在回收的状态了。

未完持续…