Objective-C 回顾【三】之 内存管理
理解引用计数
Objective-C 语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其计数。计数变为 O,就表示没人关注此对象了,于是,就可以把它销毁。
引用计数工作原理
Objective-C 中,调用 alloc
方法所返回的对象由调用者所拥有。也就是说,调用者已通过 alloc
方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是 1。在 alloc
或。initWithInt:
方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于 1。能够肯定的是:保留计数至少为 1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了该计数还是递减了该计数。
1 | NSMutableArray *array = [[NSMutableArray alloc] init]; |
如上面的代码,创建完数组后,把 number
对象加人其中。调用数组的 addobject:
方法时,数组也会在 number
上调用 retain
方法,以期继续保留此对象。这时,保留计数至少为 2。接下来,代码不再需要 number
对象了,于是将其释放。现在的保留计数至少为 1。这样就不能照常使用 number
变量了。调用 release
之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道 number
对象在调用了 release
之后仍然存活。因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码
1 | NSNumber *number = [[NSNumber alloc] initWithInt:1337]; |
即便上述代码在本例中可以正常执行,也仍然不是个好办法。如果调用 release
之后,基于某些原因,其保留计数降至 O,那么 number
对象所占内存也许会回收,这样的话,再调用 NSLog
可能就将使程序崩溃了。笔者在这里只说“可能”,而没说“一定”,因为对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaiable pool)。如果执行 NSLog
时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见:因过早释放对象而导致的 bug 很难调试。
为避免在不经意间使用了无效对象,一般调用完 release
之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为“悬垂指针”。比方说,可以这样编写代码来防止此情况发生:
1 | NSNumber *number = [[NSNumber alloc] initWithInt:1337]; |
属性存取方法中的内存管理
1 | - (void)setFoo:(id)foo { |
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么,先执行的 release
操作就可能导致系统将此对象永久回收。而后续的 retain
操作则无法令这个已经彻底回收的对象“复生”,于是实例变量就成了悬垂指针(nil)。
自动释放池
autorelease
此方法会在稍后递减计数,通常是在下一次“事件循环”时递减,不过也可能执行得更早些。
此特性很有用,尤其是在方法中返回对象时更应该用它。比如:
1 | - (NSString *)stringValue { |
注意,这里不能在方法内释放 str
否则还没等方法返回,系统就把该对象回收了。这里应该调用 autorelease
,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越“方法调用边界”后一定存活。实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。改写 stringValue
方法,使用 autorelease
来释放对象:
1 | - (NSString *)stringValue { |
修改之后,stringValue
方法把 NSString
对象返回给调用者时,此对象必然存活。所以我们能够像下面这样使用它:
1 | NSString *str = [self stringValue]; |
因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以 NSLog
语句在使用 str
对象前不需要手工执行保留操作。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留,并于稍后释放:
1 | _instanceVariable = [[self stringValue] retain]; |
由此可见,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
里调用,因为此时对象已处于正在回收的状态了。
未完持续…