[OC学习笔记]内存管理
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]内存管理相关的知识,希望对你有一定的参考价值。
OC这种面向对象的语言里,内存管理是个重要概念。想要用一门语言写出内存使用效率高而且又没有bug的代码,就得掌握其内存管理模型的种种细节。
一旦理解了这些规则,你就会发现,其实OC的内存管理没有那么复杂,而且有了“自动引用计数”(Automatic Reference,ARC)之后,就变得简单了。ARC几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。
一、理解引用计数
OC语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或者递减的计数器。如果想使某个对象继续存活,那就增加其引用计数;用完了之后就递减其计数。计数变为0,就没人关注此对象了,于是就可以把它销毁。
从 Mac OS X 10.8 开始,“垃圾收集器”(garbage collector)已经正式废弃了,而ios则从未支持过垃圾收集。
已经用过ARC的人会知道:所有于引用计数有关的方法都无法编译。
(一)引用计数工作原理
在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在OC叫“保留计数”(retain count),不过也可以叫“引用计数”(reference count)。NSObject
协议声明了下面三个方法用于操作计数器:
- retain:递增保留计数
- release:递减保留计数
- autorelease:待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数。
查看保留计数的方法叫retainCount
,此方法不太有用,即使在调试时也如此,所以并不推荐用这条方法。
对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain
方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用release
或者autorelease
方法。最终当保留计数归零时,对象就回收了(deallocated),也就是说,系统会将其占用的内存标记为“可重用”(reuse)。此时,所有指向该对象的引用都变得无效了。
在对象的生命周期中,其保留计数时而递增,时而递减,最终归零。
应用程序在其生命周期会创建很多对象,这些对象都互相联系着。可能互相引用,于是,这些相互关联的对象就构成了一张“对象图”(object graph)。对象如果持有指向其他对象的强引用(strong reference),那么前者就“拥有”(own)后者。也就是说,对象想令其所引用的那些对象继续存活,就可将其“保留”。等用完了之后再释放。
如图,ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectB与ObjectC继续存活,而应用程序里又有另一些对象想令那些对象继续存活。如果按照“引用树”回溯,那么最终会发现一个“跟对象”(root object)。在Mac OS X程序中,此对象就是NSApplicaton对象;在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时创建的单例。
下列代码有助于理解:
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
//do something with 'array'
[array release];
如前所述,如上代码在ARC下无法编译。在OC中,调用alloc方法所返回的对象由调用者所拥有。在alloc
或initWithInt:
方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会打于1。能够肯定的是:保留计数至少为1。不应该说保留计数一定是某个值,只能说所执行的操作是递增了该计数还是递减了该计数。
创建完数组后,把number对象加入其中。调用数组的“addObject:”方法时,数组也会在number
上调用retain
方法,以期继续保留此对象。这时,保留计数至少为2。接下来,代码不再需要number
对象了,于是将其释放。现在的保留计数至少为1。这样就不能照常使用number
变量了。调用release
后,已经无法保证所指的对象仍然存活,因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
NSLog(@"number = %@", number);
即使上述代码在本例中可以正常执行,也仍然不是个好方法。如果调用release
之后,基于某些原因,其保留计数降至0,那么number
对象所占内存也许会回收,这样的话,再调用NSLog
可能就使程序崩溃了。对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaiable pool)。如果执行NSLog
时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见:因过早释放对象而导致的bug很难调试。
为避免在不经意间使用了无效对象,一般调用了release
之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为“悬垂指针”(dangling pointer)。比方说,可以这样编写来防止其发生:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;
(二)属性存取方法中的内存管理
如前所述,对象图由相互关联的对象所构成。刚才那个例子中的数组通过在其元素上调用retain
方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问“属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“strong关系”(strong relationship),则设置的属性值会保留。比如说,有个名叫foo
的属性由名为_foo
的实例变量所实现,那么,该属性的设置方法会是这样:
- (void)setFoo:(id)foo
[foo retain];
[_foo release];
_foo = foo;
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一对象,那么先执行的release
操作就可能导致系统将此对象永久回收。而后续的retain
操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬垂指针。
(三)自动释放池
在OC的引用计数架构中,自动释放池是一项重要特性。调用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)后一定存活。实际上,释放操作会清空最外层的自动释放池时执行,除非有自己的释放池,否则这个时机指的就是当前进程下一次事件循环。改写方法,使用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];
//...
[_instance release];
由此可见,autorelease
能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间。
(四)保留环
使用引用计数机制时,经常要注意的一个问题就是“保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄露,因为循环中的对象其保留计数不会为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。如图,在这个循环里,所有对象的保留计数都是1。
在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isolation)。此时,垃圾收集器会把三个对象全部都收走。而在OC的引用计数架构中,则享受不到这种便利。通常采用“弱引用”(weak reference)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,避免内存泄露。
二、以ARC简化引用计数
引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。Clang编译器项目带有一个“静态分析器”(static analyzer),用于指明程序里引用计数出问题的地方。如下面手工管理引用计数:
if ([self shouldLogMessage])
NSString *message = [[NSString alloc] initWithFormat:@"I am object, %@", self];
NSLog(@"message = %@", message);
此代码有内存泄露问题,因为if
语句末尾未释放message
对象。由于在if
语句外无法引用message
对象,所以此对象所占的内存泄露了(没有正确释放已经不再使用的内存)。判定内存泄露的规则很简明,所以计算机可以很简单地将其套用在程序上,从而分析出有内存泄露问题的对象。这正是“静态分析器”要做的事。
静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或者释放操作以避免问题。自动引用计数这一思路正是源于此。自动引用计数所做的事与其名称相符,就是自动管理引用计数。于是,在前面那段代码的if
语句块结束之前,可以于message
对象上自动执行release
操作,也就是把代码自动改写为下列形式:
if ([self shouldLogMessage])
NSString *message = [[NSString alloc] initWithFormat:@"I am object, %@", self];
NSLog(@"message = %@", message);
[message release];//Added by ARC
使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在由ARC自动为你添加。除了为方法所返回的对象正确运用内存管理语义之外,ARC还有更多的功能。不过,ARC的那些功能都是基于核心的内存管理语义而构建成的,这套标准语义贯穿于整个OC语言。
由于ARC会自动执行retain
、release
、autorelease
等操作,所以直接在ARC下调用这些方法是非法的,具体来说不能调用下面方法:
- retain
- release
- autorelease
- dealloc
直接调用上述任何方法多会产生编译错误。此时必须信赖ARC,令其帮你正确处理内存管理。
实际上,ARC在调用这些方法的时候,并不通过普通的OC消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数_objc_retain。这也是不能覆写retain
、release
、autorelease
的缘由,因为这些方法从来不会被直接调用。
(一)使用ARC时必须遵循的方法命名规则
将内存管理语义在方法名中表示出来早已经成为OC的惯例,而ARC则将其确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则将其返回的对象归调用者所有:
- alloc
- new
- copy
- mutableCopy
归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了autorelease
,那么保留计数的值可能比1大,这也就是retainCount
方法不太有用的原因之一。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越其调用边界后依然有效。想要使对象多存活一段时间,必须令调用者保留它才行。
维系这些规则所需的全部内存管理事宜均由ARC自动管理,其中也包括在将要返回的对象上调用autorelease
,下列代码演示了ARC的用法:
+(EOCPerson *)newPerson
EOCPerson *person = [[EOCPerson alloc] init];
return person;
/**
* 方法名以“new”开头, 而由于“alloc” 返回的 person 已经引用计数 +1
* 所以返回时不需要 retains, release, 或 autorelease
*/
+(EOCPerson *)somePerson
EOCPerson *person = [[EOCPerson alloc] init];
return person;
/**
* 方法名不以“持有类型”的前缀命名,因此 ARC 将在返回 person 时添加 autorelease
* 对等的手动引用计数方法应写为:
* return [person autorelease];
*/
+(void)doSomething
EOCPerson *personOne = [EOCPerson newPerson];
//...
EOCPerson *personTwo = [EOCPerson somePerson];
//...
/**
* 在这里,personOne 和 personTwo 已经超出了范围,因此 ARC 需要对他们进行必要的清理
* personOne 被这段代码所持有,需要被释放
* personTwo 不被这段代码所持有,所以不需要被释放
* 对等的手动引用计数清理代码为:
* [personOne release];
*/
ARC通过命名约定将内存管理规则标准化,初学者通常觉得这很奇怪,其他编程语言很少像OC这样强调命名。但是必须适应这套理念。在编码过程中,ARC能帮程序猿🐒做很多事情。
除了会自动调用“保留”与“释放”方法外,使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够相互抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行了多次“保留”与“释放”操作,那么ARC有时可以成对的移除这两个操作。
ARC也包含运行期组件。此时执行的优化很有意义。前面讲到,某些方法在返回对象前,为其执行了autorelease操作,而调用方法的代码可能需要将返回的对象保留,如:
// _myPerson 是某类中的一个被持有的实例变量
_myPerson = [Person personWithName:@"Bob Smith"];
调用“personWithName:”方法会返回新的Person对象,而在此方法返回对象之前,为其调用了autorelease
方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理计数代码等效:
EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = [tmp retain];
此时应该能看出来,“personWithName:”方法里的autorelease
与上段代码的retain
都是多余的。为了提升性能,可以将二者删去。但是,在ARC环境下编译代码时,必须考虑“向后兼容性”(backword compatibility),以兼容那些不使用ARC的代码。其实ARC也可以直接舍弃autorelease
这个概念,并且规定,所有从方法中返回的对象其保留计数值都比期望值多1。但是,这样做就破坏了向后兼容性。
不过,ARC可以在运行期检测到这一对多余的操作,也就是autorelease
及紧跟其后的retain
。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊的函数。此时不直接调用autorelease
方法,而是改用objc_autoreleaseReturnValue
。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码要在返回的对象上执行retain
操作,则设置全局数据结构(此数据结构的具体内容因处理器而异)中的一个标志位,而不执行autorelease
操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接retain
,而是改为执行objc_retainAutoreleasedReturnValue
函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行retain
操作。设置并检测标志位,要比调用autorelease
和retain
更快。
下面这段代码演示了ARC如何通过这些特殊函数来优化程序的:
//在 Person class 中
+ (Person *) personWithName:(NSString *)name
Person *person = [[Person alloc] init];
person.name = name;
objc_autoreleaseReturnValue(person);
//使用 Person class 代码
Person *tmp = [EOCPerson personWithName:@"Matt Galloway"];
_myPerson = objc_retainAutoreleasedReturnValue(tmp);
为了求得最佳效率,这些特殊函数的实现代码因处理器而异。下面的伪代码描述了其中的步骤:
id objc_autoreleaseReturnValue(id object)
if(/* caller will retain object*/)
set_flag(object);
return object;//NO autorelease
else
return [object autorelease];
id objc_retainAutoreleaseReturnValue(id object)
if(get_flag(object))
clear_flag(object);
return object;//NO retain
else
return [object retain];
objc_autoreleaseReturnValue
函数究竟如何检测方法调用者是否会立刻保留对象呢?这要根据处理器来决定。由于必须查看元首的机器码指令方可判断这一点,所以只有编译器的作者才能实现此函数。要想判断出方法调用者会不会保留方法所返回的对象,先得把调用方法的那段代码编排好才行,而这项任务只能由编译器的开发者来完成。
将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化,上面所讲的只是其中一种。我们由此应该了解到ARC所带来的好处。待编译器与运行期组件日臻成熟,应该还会出现其他优化技术。
(二)变量的内存管理语义
ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。一定要理解这个问题,尤其要注意实例变量的语义,因为对于某些代码来说,其语义和手动管理引用计数时不同,如:
@interface EOCClass : NSObject
id _object;
@implementation EOCClass
- (void)setup
_object = [EOCOtherClass new];
@end
手动管理引用计数时,实例变量_object并不会自动保留其值,而在ARC环境下会这样做,也就是说,若在ARC下编译setup方法,其代码会变为:
- (void)setup
id temp = [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;
不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版运行期程序时,加了__weak
修饰符的weak
引用才会自动清空,因为实现自动清空操作,要用到新版所添加的一些功能。
我们经常会给局部变量加上修饰符,用以打破由“块”(block)所引入的“保留环”(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致“保留环”。可以用__weak
局部变量来打破这种“保留环”:
NSURL *url = [NSURL URLWithString:@"http://www.example.com/"];
MyNetworkFetcher *fetcher = [[MyNetworkFetcher alloc] initWithURL:url];
MyNetworkFetcher * __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则借助此特性,在该方法中生成清理内存所需的代码。
不过,如果有非OC的对象,比如CoreFoundation中的对象或是由malloc()
分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的dealloc
方法。前文说过,在ARC下不能直接调用dealloc
。ARC会自动在.cxx_destruct
方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的dealloc
方法。ARC下,dealloc
方法可以像这样来写:
- (void) dealloc
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
因为ARC会自动生成回收对象时所执行的代码,所以通常无需再编写dealloc
方法。这样可以减少项目源代码大小,省去一些样板代码(biolerplate code)。
(四)覆写内存管理方法
不使用ARC时可以覆写内存管理方法。但在ARC下不可以,因为会干扰ARC分析对象生命周期的工作。
三、在dealloc方法中只释放引用并解除监听
对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc
方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为0的时候。然而具体何时执行,则无法保证。也可以理解成:我们能够通过人工观察保留操作与释放操作的位置,来预估此方法何时即将执行。但实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。决不应该自己调用dealloc 方法。运行期系统会在适当的时候调用它。而且,一旦调用过dealloc
之后,对象就不再有效了,后续方法调用均是无效的。
那么,应该在dealloc
方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把所有 OC对象都释放掉,ARC会通过自动生成的.cxx_destruct
方法,在dealloc
中为你自动添加这些释放代码。对象所拥有的其他非OC对象也要释放。比如CoreFoundation对象就必须手工释放,因为它们是由纯C的API所生成的。
在dealloc
方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior)都清理掉。如果用NSNotificationCenter
给此对象订阅(register)过某种通知,那么一般应该在这里注销(unregister),这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。
dealloc
方法可以这样来写:
- (void)dealloc
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
请注意,如果手动管理引用计数而不使用ARC的话,那么最后还需调用[super dealloc]
。ARC会自动执行此操作,这再次表明其比手动管理更简单、更安全。若选择手动管理,则还要将当前对象所拥有的全部OC对象逐个释放。
虽说应该于dealloc
中释放引用,但是开销较大或系统内稀缺的资源则不在此列。像是文件描述符(file descriptor)、套接字(socket)、大块内存等,都属于这种资源。不能指望dealloc
方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。在这种情况下,如果非要等到系统调用dealloc
方法时才释放,那么保留这些稀缺资源的时间就有些过长了,这么做不合适。通常的做法是,实现另外一个方法,当应用程序用完资源对象后,就调用此方法。这样一来,资源对象的生命期就变得更为明确了。
比方说,如果某对象管理着连接服务器所用的套接字,那么也许就需要这种“清理方法”(cleanup method)。此对象可能要通过套接字连接到数据库。对于对象所属的类,其接口可以这样写:
#import <Foundation/Foundation.h>
@interface ServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end
该类与开发者之间的约定是:想打开连接,就调用open:
方法;连接使用完毕,就调用close
方法。“关闭”操作必须在系统把连接对象回收之前调用,否则就是编程错误(programmer error),这与通过“保留”及“释放”操作来平衡引用计数是类似的。
在清理方法而非dealloc
方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的dealloc
都会执行。极个别情况下, 当应用程序终止时,仍有对象处于存活状态,这 些对象没有收到dealloc
消息。由于应用程序终止之后,其占用的资源也会返还给操作系统,所以实际上这些对象也就等于是消亡了。不不调用dealloc方法是为了优化程序效率。而这也说明系统未必会在每个对象上调用其dealloc
方法。在Mac OS X及iOS应用程序所对应的application delegate
中,都含有一个会于程序终止时调用的方法。如果一定要清理某些对象,那么可在此方法中调用那些对象的“清理方法”。
Mac OS X:
- (void)applicationWillTerminate:(NSNotification*)notification
iOS:
- (void)applicationWillTerminate:(UINotification*)notification
如果对象管理着某些资源,那么在dealloc
中也要调用“清理方法”,以防开发者忘了清理这些资源。忘记清理资源的情况经常会发生,所以最好能输出一行消息,提示程序员代码 里含有编程错误。在系统回收对象之前,必须调用close
以释放其资源,否则close
方法就失去意义了,因此,没有适时调用close
方法就是编程错误。输出错误消息可促使开发者纠正此问题。而且,在程序员忘记调用close
的情况下,我们应该在dealloc
中补上这次调用,以防泄漏内存。下面举例说明close
与dealloc
方法应如何来写:
- (void)close
//清理资源
_closed = YES;
- (void)dealloc
if (_closed)
NSLog(@"Error: close was not called before dealloc!");
[self close];
有时可能要抛出异常来表明不调用close
方法是严重编程错误。
编写dealloc
方法时还要注意,不要在里面随便调用其他方法。上面的dealloc
方法确实调用了另外一个方法,不过那是为了侦测编程错误而破例。无论在这里调用什么方法都不太应该,因为对象此时“已近尾声”(in awinding-down state)。如果在这里所用的方法又要异步执行某些任务,或是又要继续调用它们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。这会导致很多问题,且经常使应用程序崩溃。因为那些任务执行完毕后,要回调此对象,告诉该对象任务已完成,而此时如果对象已摧毁,那么回调操作就会出错。
再注意一个问题:调用dealloc
方法的那个线程会执行“最终的释放操作”(final release),令对象的保留计数降为0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在dealloc
里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于“正在回收的状态”(deallocating state),为了指明此状况,运行期系统已经改动了对象内部的数据结构。
在dealloc
里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于“键值观测”(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时“保留”或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些奇怪的错误。
要点
- 在
dealloc
方法里,应该做的事情就是释放打指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或NSNotificationCenter
等通知,不要做其他事情。 - 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必多须调用
close
方法。 - 执行异步任务的方法不应在
dealloc
里调用; 只能在正常状态下执行的那些方法也不 应在dealloc
里调用,因为此时对象已处于正在回收的状态了。
四、编写“异常安全代码”时留意内存管理问题
纯C中没有异常,C++和OC都支持异常。在当前运行期系统中,C++和OC的异常互相兼容,也就是说,从其中一门语言抛出的异常能用另一门语言的“异常处理程序”(exception handler)来捕获。
OC错误模型表示,异常只应在发生严重错误后抛出,虽说如此,有时候仍需要编写代码来捕获并处理异常。比如第三方库、有些系统库或者使用KVO时。
发生异常时应该如何管理内存是个值得研究的问题。在try
块中,如果先保留了某个对象,然后在释放它前又抛出了异常,那么,除非catch
块能处理此问题,否则对象所占内存就将泄露。
异常处理例程将自动销毁对象,然而在手动管理引用计数时,销毁工作有些麻烦。以下是使用手工引用计数的OC代码:
@try
SomeClass *object = [[SomeClass alloc] init];
[object doSomethingThatMayThrow];
[object release][OC学习笔记]ARC与引用计数