理解引用计数
前言: OC 语言使用引用计数来管理内存,也就是说, 每个对象都有个可以递增或递减的计数器. 如果想使某个对象继续存活, 那就递增其引用计数; 用完了之后, 就递减其引用计数. 当计数变为0时, 对象就会被销毁了.
ARC实际上就是一种引用计数机制.
要点
1. 引用计数机制通过可以递增递减的计数器来管理内存. 对象创建好之后, 其保留计数至少为1. 若保留计数为正, 则对象继续存活. 当保留计数降为0时, 对象就被销毁了.
2. 在对象生命周期中, 其余对象通过引用来保留或释放此对象. 保留与释放操作分别会递增或递减保留计数.
1.引用计数工作原理
在引用计数架构下, 对象有个计数器, 用以表示当前有多少个事物想令此对象继续存活下去. 在OC中, 叫"保留计数"(retain count), 也可以叫"引用计数"(reference count).
NSObject协议声明了以下三个方法用于操作计数器,以递增或递减其值:
- retain : 递增保留计数
- release : 递减保留计数
- autorelease : 待稍后清理"自动释放池"(autorelease pool)时, 再递减保留计数.
查看保留计数的方法: retainCount, 这方法不太管用,在调试时也是如此,苹果公司并不建议使用
对象创建出来时,其保留计数至少为1. 若想令对象继续存活,则调用retain方法. 若不再使用此对象, 就调用release或autorelease方法. 当保留计数为0时,对象就被回收了, 系统会将其占用的内存标记为"可重用". 此时,所有指向该对象的引用就无效了.
在对象生命周期中, 其保留计数时而递增, 时而递减, 最终归零
应用程序在其生命周期中会创建很多对象, 这些对象都相互联系着. 例如下图中: 对象B与对象C 都引用了对象A. 若对象B与对象C都不再使用对象A, 则其保留计数减为0, 于是对象A被销毁了.
对象图里所有指向对象A的引用均释放之后, 对象A所占内存亦可回收
图中还有其他对象引用对象B与对象C, 而应用程序里又有另外一些对象引用其他对象. 如果按"引用树"回溯,最终会发现一个"根对象". 在iOS应用程序中, 是UIApplication对象. Max OS中则是NSApplication对象,两者都是应用程序启动时创建的单例.
下面这段代码有助于理解这些方法的用法:
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2019];
[array addObject:number];
[number release];
// do something with 'array'
[array release];
在ARC下, 无法编译release方法; 在OC中, 调用alloc方法所返回的对象由调用者拥有. 对象引用计数加1, 注意: 这并不是说对象此时的保留计数必定是1
. 在alloc 或 initWithInt: 方法实现代码中, 也许还有其他对象引用了此对象, 所以其保留计数可能会大于1. 能够肯定的是: 其保留计数至少为1
. 保留计数的概念应该这样理解, 绝不应该说保留计数一定是某个值, 只能说你执行的操作递增或递减了该计数
.
创建完数组后, 把number对象加入其中, 调用数组的 addObject: 方法时, 数组也会在 number上调用 retain 方法, 来继续保留该对象. 这时计数至少为2. 接着释放 number 对象, 保留的计数至少为1, 调用 release 之后, 已经无法保证所指的number 对象是否存活, 也就不能正常使用 number变量了. 当前本例中,数组还引用着number对象, 在调用release之后依然存活, 然而绝不应假设此对象一定存活, 也就是说不能像下面那样编码:
[number release];
NSLog(@"number= %@", number);
如果由于某些原因, 其引用计数减为0, 那么number对象所占内存也许会被回收, 此时调用 NSLog 可能会使程序崩溃!!!
为避免不经意间使用了无效对象, 一般用完release 之后都会清空指针. 保证不会出现可能指向无效对象的指针, 这种指针通常称为"悬挂指针".
可以照下面编码来防止此情况发生:
[number release];
number = nil;
2.属性存取方法中的内存管理
访问属性时, 会用到相关实例变量的获取方法及设置方法. 若属性为"strong"关系, 则设置的属性值会保留.
例: 有个名叫 foo 的属性由名为 _foo 的实例属性所实现, 那么,该属性设置方法会是这样的:
- (void)setFoo:(id)foo {
[foo retain]; // 保留新值
[_foo release]; // 释放旧值
_foo = foo;
}
此方法将保留新值并释放旧值, 然后更新实例变量, 令其指向新值. 顺序很重要, 假如还未保留新值就先把旧值释放了, 而且两个值又指向同一个对象, 那么, 先执行release操作就可能导致系统将此对象永久回收. 而后续的retain操作无法令这个已经回收的对象复生, 于是实例变量也就成了悬挂指针.
3. 自动释放池
在OC的引用计数架构中, 自动释放池是一个重要特性. 调用release 会立刻递减对象的保留计数(而且可能令系统回收此对象), 有时候可以改为调用 autorelease, 此方法会在稍后递减计数, 通常是在下一次"事件循环"时递减, 不过也可能执行更早些.
此特性很有用, 尤其在方法中返回对象时; 以下面方法为例:
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
return str;
}
此时返回的str对象其保留计数比期望值多1, 因为调用alloc会令保留计数加1, 而又没有与之对应的释放操作. 有retain,就应该有对应的release, 将其抵消. 这并不是说保留计数就一定是1, 可能大于1(这取决于initWithFormat:方法内部实现).
但是,不能在方法内释放str,否则还没等方法返回, 系统就把该对象回收了. 这里应该用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对象将于稍后自动释放,多出来的那一次retain操作到时自然就会抵消,无需再执行内存管理操作. 因为自动释放池中的释放操作要等到下一次事件循环时才会执行.
autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间.
4. 循环引用
使用引用计数机制时, 经常要注意一个问题就是: 循环引用; 也就是呈环状相互引用的多个对象. 这将导致内存泄漏, 因为循环中的对象其保留计数不会降为0.
通常采用"弱引用"来解决此问题, 或是从外界命令循环中的某个对象不再保留另外一个对象. 这两种方法都能打破保留环, 避免内存泄漏.