前言
在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协议声明了下面三个方法用于操作计数器,以递增或递减其值:
retain 递增保留计数release 递减保留计数autorelease 待稍后清理“自动释放池”时,再递减保留计数。
- 查看保留计数的方法叫
retainCount ,此方法不太有用,即便在调试时也如此,所以并不推荐大家使用这个方法。 - 对象创建出来时,其保留计数至少为1.若想令其继续存活,则调用retain方法。若是某部分代码不在使用此对象不想令其继续存活,那就调用
release 或autorelease 方法,最终当保留计数归零时,对象就回收了。也就是说,系统会将其占用的内存标记为“可重用”。此时,所有指向该对象的引用也都变得无效了。
如图演示了对象自创造出来之后历经一次“保留”及两次“释放”操作的过程。
- 应用程序在其生命期中会创建很多对象,这些对象都相互联系着。
在下方图中,ObjectB 与ObjectC 都引用了ObjectA 。若ObjectB 与ObjectC 都不再使用ObjectA ,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectB 与ObjectC 继续存活,而应用程序里又又另外一些对象想令那些对象继续存活。如果按“引用树”回溯,那么最终会发现一个“根对象”。在Mac OS X应用程序中,此对象就是NSApplication 对象;而在iOS应用程序中,则是UIApplication 对象。两者都是应用程序启动时所创建的单例。
下面这段代码有助于理解这些方法的用法:
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
[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。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
以ARC简化引用计数
引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。所以Clang 编译器项目带有一个"静态分析器"(static analyzer),用于指明程序里引用计数出问题的地方。举个例子,假设下面这段代码采用手工方式管理引用计数∶
if ([self shouldLogMessage]) {
NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
NSLog(@"message = %@", message);
}
- 此代码有内存泄漏问题,因为if语句块末尾并未释放
message 对象。由于在 if 语句之外无法引用message ,所以此对象所占的内存泄漏了。判定内存是否泄漏所用的规则很简明:调用NSString 的 alloc 方法所返回的那个 message 对象的保留计数比期望值要多1。然而却没有与之对应的释放操作来抵消。因为这些规则很容易表述,所以计算机可以简单地将其套用在程序上。 从而分析出有内存泄漏问题的对象。这正是**“静态分析器”**要做的事。
静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题,是不是这样子呢?自动引用计数这一思路正是源于此。自动引用计数所做的事情与其名称相符。就是白动管理引用计数。干是,在前面那段代码的if 语句块结束之前,可以于 message 对象上自动执行 release 操作,也就是把代码自动改写为下列形式∶
if ([self shouldLogMessage]) {
NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
NSLog(@"message = %@", message);
[message release];
}
- 使用 ARC 时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由 ARC 自动为你添加。稍后将会看到,除了为方法所返回的对象正确运用内存管理语义之外,ARC还有更多的功能。不过,ARC的那些功能都是基于核心的内存管理语义而构建的,这套标准语义贯穿于整个
Objective-C 语言。 由于 ARC会自动执行 retain 、release 、autorelease 等操作,所以直接在 ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法∶
1. retain 2. release 3. autorelease 4. dealloc
直接调用上述任何方法都会产生编译错误,因为 ARC要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。此时必须信赖 ARC,令其帮你正确处理内存管理事宜,而这会使那些惯于手动管理引用计数的开发者不太放心。 实际上,ARC在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。比方说,ARC会调用与retain 等价的底层函数 objc retain 。这也是不能覆写retain 、release 或 autorelease 的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的Objective-C 方法来指代与之相关的底层C语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。
使用ARC时必须遵循的方法命名规则
- 将内存管理语义在方法名中表示出来早已成为
Objective-C 的惯例,而 ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有∶
1. alloc 2. new 3. copy 4. mutableCopy
- 归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了
autorelease ,那么保留计数的值可能比1大,这也是 retainCount 方法不太有用的原因之一。 - 若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
- 维系这些规则所需的全部内存管理事宜均由 ARC 自动处理,其中也包括在将要返回的对象上调用
autorelease ,下列代码演示了ARC的用法∶
+ (EOCPerson *)newPerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}
+ (EOCPerson *)somePerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}
- (void)doSomething {
EOCPerson *personOne = [EOCPerson newPerson];
EOCPerson *personTwo = [EOCPerson somePerson];
}
- ARC通过命名约定将内存管理规则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像
Objective-C 这样强调命名。但是,想成为优秀的 Objective-C 程序员就必须适应这套理念。在编码过程中,ARC 能帮程序员做许多事情。 - 除了会自动调用"保留"与"释放"方法外,使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。 例如,在编译期,ARC会把能够互相抵消的
retain 、release 、autorelease 操作约简。如果发现在同一个对象上执行了多次"保留"与"释放"操作,那么 ARC有时可以成对地移除这两个操作。
变量的内存管理语义
- ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。一定要理解这个问题,尤其要注意实例变量的语义,因为对于某些代码来说,其语义和手动管理引用计数时不同。
例如,有下面这段代码:
@interface EOCClass : NSObject {
id _object;
}
@implementation EOCClass
- (void) setup {
_object = [EOCOtherClass new];
}
@end
在手动管理引用计数时,实例变量 object 并不会自动保留其值,而在 ARC环境下则会这样做。也就是说,若在 ARC下编译 setup 方法,则其代码会变为∶
- (void) setup {
id tmp = [EOCOtherClass new];
_object = [tmp retain];
[tmp release];
}
当然,在此情况下,retain 和 release 可以消去。所以,ARC会将这两个操作化简掉,于是,实际执行的代码还是和原来一样。不过,在编写设置方法(setter )时,使用ARC会简单一些。如果不用ARC,那么需要像下面这样来写∶
- (void) setObject:(id)object {
[_object release];
_object = [object retain];
}
但是这样写会出问题。假如新值和实例变量已有的值相同,会如何呢? 如果只有当前对象还在引用这个值,那么设置方法中的释放操作会使该值的保留计数降为0,从而导致系统将其回收。接下来再执行保留操作,就会令应用程序崩溃。使用ARC之后,就不可能发生这种疏失了。在 ARC环境下,与刚才等效的设置函数可以这么写∶
- (void) setObject:(id)object {
_object = object;
}
ARC会用一种安全的方式来设置;先保留新值,再释放旧值,最后设置实例变量。在手动管理引用计数时,你可能已经明白这个问题了,所以应该能正确编写设置方法,不过用了ARC之后,根本无须考虑这种"边界情况"(edge case)。 在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong ∶默认语义,保留此值。__unsafe_unretained ∶不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。weak ∶不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。__autoreleasing ∶把对象"按引用传递"(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
比方说,想令实例变量的语义与不使用ARC时相同,可以运用weak 或unsafe_unretained 修饰符∶
@interface EOCClass : NSObject {
id _weak _weakObject;
id unsafe unretained unsafeUnretainedObject;
}
不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版(Mac OS X 10.7、iOS 5.0及其后续版本)运行期程序库时,加了 weak修饰符的 weak引用才会自动清空,因为实现自动清空操作,要用到新版所添加的一些功能。
- 我们经常会给局部变量加上修饰符,用以打破由"块"(block,参见第 40条)所引入的"保留环"(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致"保留环"。可以用 weak 局部变量来打破这种"保留环"∶
NSURL*url = [NSURL URLWithString:@"http:
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
EOCNetworkFetcher * weak weakFetcher = fetcher;
[fetcher startWithCompletion:^(BOOL success){
NSLog(@"Finished fetching from %@",weakFetcher.url);
}];
ARC如何清理实例变量
刚才说过,ARC也负责对实例变量进行内存管理。要管理其内存,ARC就必须在"回收分配给对象的内存’(deallocate )三时生成必要的清理代码(cleanup code)。凡是具备强引用的变量,都必须释放,ARC会在 dealloc 方法中插入这些代码。当手动管理引用计数时,你可能会像下面这样自己来编写 dealloc 方法∶
- (void) dealloc {
[_foo release];
[_bar release];
[super dealloc];
}
用了ARC之后,就不需要再编写这种 dealloc 方法了,因为 ARC会借用Objective-C++ 的一项特性来生成清理例程(cleanup routine)。回收 Objective-C++ 对象时,待回收的对象会调用所有C++对象的析构函数(destructor )。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx destruct 的方法。而 ARC则借助此特性,在该方法中生成清理内存所需的代码。
不过,如果有非 Objective-C的对象,比如 CoreFoundation 中的对象或是由 malloc 分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过,在 ARC下不能直接调用dealloc 。ARC会自动在.cxx destruct 方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的 dealloc 方法。ARC环境下,dealloc 方法可以像这样来写∶
-(void) dealloc(
CFRelease ( coreFoundationObject);
free ( heapAllocatedMemoryBlob);
}
- 因为 ARC会自动生成回收对象时所执行的代码,所以通常无须再编写
dealloc 方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码(boilerplate code)。
覆盖内存管理方法
- 不使用ARC 时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写
release 方法,将其替换为"空操作"(no-op)。但在 ARC环境下不能这么做,因为会干扰到ARC分析对象生命期的工作。而且,由于开发者不可调用及覆写这些方法,所以 ARC能够优化retain 、release 、autorelease 操作,使之不经过 Objective-C 的消息派发机制(参见第 11条)。优化后的操作,直接调用隐藏在运行期程序库中的C 函数。这就意味着 ARC可以执行各种优化了,比如刚才提到;如果方法命令即将返回的对象稍后"自动释放",而方法调用者立刻"保留"这个返回后的对象,那么这两个操作就会为 ARC 所化简。
要点
- 有 ARC之后,程序员就无须担心内存管理问题了。使用 ARC来编程,可省去类中的许多"样板代码"。
- ARC 管理对象生命期的办法基本上就是∶在合适的地方插入"保留"及"释放"操作。在 ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行"保留"及"释放"操作。
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。
- ARC只负责管理
Objective-C 对象的内存。尤其要注意∶CoreFoundation 对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease 。
在dealloc方法中只释放引用并接触监听
对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc 方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为0的时候。然而具体何时执行,则无法保证。也可以理解成;我们能够通过人工观察保留操作与释放操作的位置,来预估此方法何时即将执行。但实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc方法。运行期系统会在适当的时候调用它。而且,一旦调用过 dealloc 之后,对象就不再有效了,后续方法调用均是无效的。 那么,应该在 dealloc 方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把所有 Objective-C对象都释放掉,ARC会通过自动生成的.cxx destruct 方法(参见第30条),在 dealloc 中为你自动添加这些释放代码。对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放,因为它们是由纯 C 的 API所生成的。 在 dealloc方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior)都清理掉。如果用NSNotificationCenter给此对象订阅(register)过某种通知,那么一般应该在这里注销(unregister),这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。 dealloc 方法可以这样来写∶
-(void)dealloC {
CFRelease (coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 请注意,如果手动管理引用计数而不使用ARC的话,那么最后还需调用"
[super dealloc] "。ARC会自动执行此操作,这再次表明其比手动管理更简单、更安全。若选择手动管理,则还要将当前对象所拥有的全部Objective-C 对象逐个释放。 - 虽说应该于
dealloc 中释放引用,但是开销较大或系统内稀缺的资源则不在此列。像是文件描述符(file descriptor)、套接字(socket)、大块内存等,都属于这种资源。不能指望dealloc 方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。在这种情况下,如果非要等到系统调用 dealloc 方法时才释放,那么保留这些稀缺资源的时间就有些过长了,这么做不合适。通常的做法是,实现另外一个方法,当应用程序用完资源对象后,就调用此方法。这样一来,资源对象的生命期就变得更为明确了。
在 Mac OS X系统里,应用程序终止时会调用NSApplicationDelegate 之中的下述方法∶
- (void)applicationWillTerminate:(NSNotification *)notification
而在 iOS系统里,应用程序终止时则会调用 UIApplicationDelegate 之中的下述方法∶
- (void)applicationWillTerminate:(UIApplication *) application
- 如果对象管理着某些资源,那么在
dealloc 中也要调用"清理方法",以防开发者忘了清理这些资源。忘记清理资源的情况经常会发生,所以最好能输出一行消息,提示程序员代码里含有编程错误。在系统回收对象之前,必须调用close 以释放其资源,否则 close 方法就失去意义了,因此,没有适时调用close 方法就是编程错误。输出错误消息可促使开发者纠正此问题。而目,在程序员忘记调用close 的情况下。我们应该在 dealloc 中补上这次调用。以防泄漏内存。 - 编写
dealloc 方法时还需注意,不要在里面随便调用其他方法。刚才那段范例代码中,dealloc 方法确实调用了另外一个方法,不过那是为了侦测编程错误而破例。无论在这里调用什么方法都不太应该,因为对象此时"已近尾声"(in a winding-down state)。如果在这里所调用的方法又要异步执行某些任务,或是又要继续调用它们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。这会导致很多问题,且经常使应用程序崩溃,因为那些任务执行完毕后,要回调此对象,告诉该对象任务已完成,而此时如果对象已摧毁,那么回调操作就会出错。 - 请再注意一个问题;调用
dealloc 方法的那个线程会执行"最终的释放操作"(final release),令对象的保留计数降为0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在 dealloc里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于"正在回收的状态"(deallocating state),为了指明此状况,运行期系统已经改动了对象内部的数据结构。 - 在
dealloc 里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于"键值观测"(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时"保留"或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。
要点
- 在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的"键值观测"(KVO)或
NSNotificationCenter 等通知,不要做其他事情。 - 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定∶用完资源后必须调用
close 方法。 - 执行异步任务的方法不应在
dealloc 里调用;只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已处于正在回收的状态了。
编写“异常安全代码”时留意内存管理问题
- 许多时下流行的编程语言都提供了"异常"(exception)这一特性。纯C中没有异常,而
C++ 与Objective-C 都支持异常。实际上,在当前的运行期系统中,C++ 与Objective-C 的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编的"异常处理程序"(exception handler)来捕获。 Objective-C 的错误模型表明,异常只应在发生严重错误后抛出(参见第 21条),虽说如此,不过有时仍然需要编写代码来捕获并处理异常。比如使用Objective-C++ 来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,这使我们想起从前那个频繁使用异常的年代。比如,在使用"键值观测"(KVO)功能时,若想注销一个尚未注册的"观察者",便会抛出异常。- 发生异常时应该如何管理内存是个值得研究的问题。在 try 块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非 catch块能处理此问题,否则对象所占内存就将泄漏。
C++ 的析构函数(destructor)由 Objective-C 的异常处理例程(exception-handle routine)来运行。这对于C++对象很重要,由于抛出异常会缩短其生命期,所以发生异常时必须析构,不然就会泄漏,而文件句柄(file handle)等系统资源因为没有正确清理,所以就更容易因此而泄漏了。 异常处理例程将自动销毁对象,然而在手动管理弓用计数时。销毁工作有些麻烦。
以下面这段使用手工引用计数的 Objective-C 代码为例∶
@try {
EOCSomeClass *object = [ [EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
[object release];
}
@catch(...) {
NSLog(@"whoops,there was an error. Oh well...");
}
乍一看似乎没问题,但如果 doSomethingThatMayThrow 抛出异常了呢?由于异常会令执行过程终止并跳至 catch 块,因而其后的那行 release 代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄漏了。这么做不好。解决办法是使用@finally 块,无论是否抛出异堂其中的代码都保证会运行,日 只运行一次。H方说,刚才那段代码可改写如下:
EOCSomeClass *object;
@try {
object = [ [EOCSomeClass alloc] init];
[object doSomethingThatMayThrow] ;
}
@catch (...){
NSLog (@"whoops,there was an error.Oh well...");
}
@finally {
[object release];
}
- 注意,由于
@finally 块也要引用object 对象,所以必须把它从 @try 块里移到外面去。要是所有对象都得如此释放,那这样做就会非常乏味。而且,假如 @try 块中的逻辑更为复杂,含有多条语句,那么很容易就会因为忘记某个对象而导致泄漏。若泄漏的对象是文件描述符或数据库连接等稀缺资源(或是这些稀缺资源的管理者),则可能引发大问题,因为这将导致应用程序把所有系统资源都抓在自己手里而不及时释放。
在 ARC 环境下,问题会更严重。下面这段使用ARC 的代码与修改前的那段代码等效∶
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow ];
}
@catch (...) {
NSLog(@"whoops,there was an error. Oh well...");
}
- 现在问题更大了∶由于不能调用
release ,所以无法像手动管理引用计数时那样把释放操作移到 @finally 块中。你可能认为这种状况 ARC自然会处理的。但实际上 ARC不会自动处理,因为这样做需要加入大量样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛异常时也如此。而且,添加进来的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。 - 虽说默认状况下未开启,但 ARC 依然能生成这种安全处理异常所用的附加代码。
-fobjc-arc-exceptions 这个编译器标志用来开启此功能。其默认不开启的原因是;在 Objective-C 代码中,只有当应用程序必须因异常状况而终止时才应抛出异常(参见第 21条)。因此,如果应用程序即将终止,那么是否还会发生内存泄漏就已经无关紧要了。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。 - 有种情况编译器会自动把
-fobjc-arc-exceptions 标志打开,就是处于 Objective-C++ 模式时。因为C++ 处理异常所用的代码与 ARC实现的附加代码类似,所以令 ARC加入自己的代码以安全处理异常,其性能损失并不太大。此外。由于C++ 频繁使用异常,所以Objective-C++ 程序员很可能也会使用异常。 - 如果手工管理引用计数,而且必须捕获异常,那么要设法保证所编代码能把对象正确清理干净。若使用ARC且必须捕获异常,则需打开编译器的-
fobjc-arc-exceptions 标志。但最重要的是;在发现大量异常捕获操作时,应考虑重构代码,用第 21条所讲的NSError 式错误信息传递法来取代异常。
要点
- 捕获异常时,一定要注意将 try 块内所创立的对象清理干净。
- 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
以弱引用避免保留环
- 对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成 “环”(cycle)。由于
Objective-C 内存管理模型使用引用计数架构,所以这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得它们都能继续存活下去,而不会为系统所回收。 - 最简单的保留环由两个对象构成,它们互相引用对方。如图举例说明了这种情况。这种保留环的产生原因不难理解,且很容易就能通过查看代码而侦测出来∶
#import<Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA : NSObject
@pzoperty(nonatomic,strong) EOCClassB *other;
@end
@interface EOCClassB : NSObject
@property(nonatomic,strong) EOCClassA *other;
@end
- 看代码很容易就能发现其中可能出现的保留环∶如果把
EOCClassA 实例的 other 属性设置成某个EOCClassB 实例,而把那个EOCClassB 实例的 other 属性又设置成这个EOCClassA 实例,那么就会出现下图的保留环:
- 保留环中只剩一个对象还为对象图里的其他对象所引用,移除此引用后整个保留环就泄漏了
- 保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。也就是说,没办法再访问其中的对象了。图 5-5所示的保留环更复杂一些,其中有四个对象,只有
ObjectB 还为外界所引用,把仅有的这个引用移除之后,四者所占内存就泄漏了。 - Mac OS X平台的
Objective-C 程序有个选项,可以启用垃圾收集器(garbage collector),它会检测保留环,若发现外界不再引用其中的对象,则将之回收。但是,从 Mac OS X 10.8 开始。垃圾收集机制就废弃了。而目iOS系统从未支持过这项功能。因此。从一开始编码时就要注意别出现保留环。 - 避免保留环的最佳方式就是弱引用。这种引用经常用来表示"非拥有关系"(nonowning relationship)。将属性声明为
unsafe_unretained 即可。
修改刚才那段范例代码,将其属性声明如下∶
#import <Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end
@interface EOCClassB: NSObject
@property(nonatomic,unsafe_unretained) EOCClassA *other;
@end
- 修改之后,
EOCClassB 实例就不再通过 other 属性来拥有 EOCClassA 实例了。属性特质(attribute)中的 unsafe_unretained 一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。 - 用
unsafe_unretained 修饰的属性特质,其语义同 assign 特质等价(参见第6条)。然而,assign 通常只用于"整体类型"(int 、float 、结构体等),unsafe_unretained 则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用(unsafe)。 Objective-C 中还有一项与ARC 相伴的运行期特性,可以令开发者安全使用弱引用∶这就是weak 属性特质,它与unsafe unretained 的作用完全相同。然而,只要系统把属性回收,属性值就会自动设为 nil。在刚才那段代码中,EOCClassB 的 other 属性可修改如下∶
@property (nonatomic,weak) EOCClassA *other;
- 一般来说,如果不拥有某对象,那就不要保留它。这条规则对
collection 例外,collection 虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。有时,对象中的引用会指向另外一个并不归自己所拥有的对象,比如 Delegate 模式就是这样
要点
- 将某些引用设为
weak ,可避免出现"保留环"。 weak 引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着 ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
以“自动释放池块”降低内存峰值
Objective-C 对象的生命期取决于其引用计数。在 Objective-C 的引用计数架构中,有一项特性叫做"自动释放池"(autorelease pool)。释放对象有两种方式;一种是调用release 方法,使其保留计数立即递减;另一种是调用 autorelease 方法,将其加入"自动释放池"中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送 release 消息。
创建自动释放池所用语法如下∶
@autoreleasepool {
}
如果在没有创建自动释放池的情况下给对象发送 autorelease 消息,那么控制台会输出这样一条信息∶
Object 0xabcd0123 of class NSCFString autoreleased with no pool in place -just leaking - break on objc_ autoreleaseNoPool () to debug
然而,一般情况下无须担心自动释放池的创建问题。Mac OS X与 iOS 应用程序分别运行于Cocoa 及 Cocoa Touch 环境中。系统会自动创建一些线程,比如说主线程或是"大中枢派发"(Grand Central Dispatch,GCD)③机制中的线程,这些线程默认都有自动释放池,每次执行"事件循环"(event loop)时,就会将其清空。因此,不需要自己来创建"自动释放池块"。通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们用自动释放池来包裹应用程序的主入口点((main application entry point)。比方说,iOS程序的 main 函数经常这样写∶
int main(int argc,char *argv[]) {
@autoreleasepool {
return UIApplicationMain (argc, argV, ni1, @"EOCAppDelegate");
}
}
从技术角度看,不是非得有个"自动释放池块"才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由 UIApplicationMain 函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。 下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并干对应的右花括号处自动清空。位于自动释放池范围内的对象。将在此范围末尾处收到release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的油里。比方说∶
@autoreleasepool {
NSString *string = [NSString stringwithFormat:@"1 = %i",1];
@autoreleasepool {
NSNumber *number = [NSNumber numberWithInt:1];
}
}
- 本例中有两个对象,它们都由类的工厂方法所创建,这样创建出来的对象会自动释放(参见第30条)。
NSString 对象放在外围的自动释放池中,而 NSNumber 对象则放在里层的自动释放池中。将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
要点
- 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
用“僵厂对象”调试内存管理问题
- 调试内存管理问题很令人头疼。大家都知道,向业已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里。而此对象也许能应答。也许不能。如果能。那程序就不崩溃,可你会觉得奇怪∶为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。
所幸 Cocoa 提供了"僵尸对象"(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的"僵尸对象"、而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。 将NSZombieEnabled 环境变量设为 YES ,即可开启此功能。比方说,在 Mac OS X系统中用 bash运行应用程序时,可以这么做∶
export NSZombieEnabled="YES"
./app
给僵尸对象发消息后。控制台会打印消息,而应用程序则会终止。打印出来的消息就像这样∶
*** -[CFString respondsToSelector:]: message sent to deallocated instance 0x7ff9e9c080e0
- 也可以在 Xcode里打开此选项,这样的话,Xcode 在运行应用程序时会自动设置环境变量。开启方法为∶编辑应用程序的 Scheme,在对话框左侧选择"Run",然后切换至"Diagnostics"分页,最后勾选"Enable Zombie Objects"选项。
那么,僵尸对象的工作原理是什么呢?它的实现代码深植于 Objective-C 的运行期程序库、Foundation 框架及 CoreFoundation 框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。
- 在完整的消息转发机制中,
forwarding 是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为 NSZombie ,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息(本条目开头曾列出),其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。在僵尸类名中嵌入原始类名的好处,这时就可以看出来了。只要把 NSZombie 从僵尸类名的开头拿掉,剩下的就是原始类名。下列伪代码演示了这一过程:
Class cls = object_getClass(self);
const char *clsName = class getName (cls);
if(string has prefix(clsName," NSZombie_"){
const char *originalClsName = substring_from (clsName,10);
const char*selectorName = sel getName ( cmd);
Log("*** -[%s %s]:message sent to deallocated instance %p",
originalClsName,selectorName,self);
abort();
}
要点
- 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量
NSZombieEnabled 可开启此功能。 - 系统会修改对象的
isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为∶打印一条包含消息内容及其接收者的消息,然后终止应用程序。
不要使用retainCount
Objective-C 通过引用计数来管理内存(参见第 29条)。每个对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。对象创建好之后,其保留计数大于0。保留与释放操作分别会使该计数递增及递减。当计数变为0时,对象就为系统所回收并摧毁了。 NSObject 协议中定义了下列方法,用于查询对象当前的保留计数∶
-(NSUInteger) retainCount
- 然而 ARC已经将此方法废弃了。实际上,如果在 ARC中调用,编译器就会报错,这和在 ARC中调用
retain 、release 、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。若在不启用ARC的环境下编程(说真的,还是在ARC下编程比较好),那么仍可调用此方法,而编译器不会报错。所以,还是必须讲清楚为何不应使用此方法。 这个方法看上去似乎挺合理、挺有用的。它毕竟返回了保留计数,而此值对每个对象来说显然都很重要。但问题在于,保留计数的绝对数值一般都与开发者所应留意的事情完全无关。即便只在调试时才调用此方法,通常也还是无所助益的。
此方法之所以无用。其首要原因在干∶它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空(参见第 34条).因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。因此,下面这种写法非常糟糕∶
while([object retainCount]){
[object release];
}
- 这种写法的第一个错误是∶它没考虑到后续的自动释放操作,只是不停地通过释放操作来降低保留计数,直至对象为系统所回收。假如此对象也在自动释放池里,那么稍后系统清空池子时还要把它再释放一次,而这将导致程序崩溃。
- 第二个错误在于:
retainCount 可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。因此,保留计数可能永远都不会完全归零。所以说,这段代码就算有时能正常运行,也多半是凭运气,而非理性判断。对象回收之后,如果 while 循环仍在运行,那么目前的运行期系统一般会直接令应用程序崩溃。
从来都不需要编写这种代码。这段代码所要实现的操作,应该通过内存管理来解决。开发者在期望系统于某处回收对象时,应该确保没有尚未抵消的保留操作,也就是不要令保留计数大于期望值。在这种情况下,如果发现某对象的内存泄漏了,那么应该检查还有谁仍然保留这个对象,并查明其为何没有释放此对象。
要点
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的"绝对保留计数"都无法反映对象生命期的全貌。
- 引入 ARC之后,
retainCount 方法就正式废止了,在 ARC下调用该方法会导致编译器报错。
|