[编写高质量iOS代码的52个有效方法](七)内存管理(上)
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹为快
29.理解引用计数
30.以ARC简化引用计数
31.在dealloc方法中只释放引用并解除监听
32.编写异常安全代码时留意内存管理问题
目录
第29条:理解引用计数
Objective-C语言使用引用计数来管理内存,也就是说每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完之后,就递减其计数。计数变为0,就表示无人关注此对象了,于是,就可以把它销毁。
在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在Objective-C中叫做保留计数,也叫引用计数。NSObject协议声明了下面3个方法用于操作计数器,以递增或递减其值(ARC中不可用):
retain // 递增保留计数
release // 递减保留计数
autorelease // 待稍后清理自动释放池时再递减保留计数
对象创建出来时,其保留计数至少为1.若想令其继续存活,则调用retain方法。要是某部分代码不再使用此对象,那就调用release或autorelease方法。最终保留计数归0时,对象就回收了。
为了避免在不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常被称为悬挂指针。
NSMutableArray *array = [[NSMutableArray alloc] init];
// number创建,保留计数为1
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
// 数组引用了number,number保留计数为2
[array addObject:number];
// 释放number,保留计数为1
[number release];
// 调用release后,无法保证number仍然存活(虽然在本例中可以明显看到number还存活),应当清空指针。
number = nil;
属性为strong时,属性的setter方法是先保留新值再释放旧值,然后更新实例变量,使其指向新值。
- (void)setFoo:(id)foo{
[foo retain];
[_foo release];
_foo = foo;
}
执行顺序呢很重要,假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么先执行的release操作就可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已彻底回收的对象复生,实例变量就成为悬挂指针。
调用release会立刻递减对象的保留计数(可能会令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数。(通常是下一次时间循环时递减)
- (NSString*)stringValue{
NSString *str = [[NSString alloc] initWithFormat:@"I am this %@",self];
return str;
}
此时返回的str对象其保留计数比期望值要多1,因为调用alloc会令保留计数加1,而又没有与之对应的释放操作。也不能在方法内释放str,否则还没等方法返回,系统就把对象回收了。这里应该使用autorelease,它会在稍后释放对象。
- (NSString*)stringValue{
NSString *str = [[NSString alloc] initWithFormat:@"I am this %@",self];
return [str autorelease];
}
第30条:以ARC简化引用计数
使用ARC时,引用计数实际上还是在执行。只不过保留与释放操作现在是由ARC自动添加。因此直接在ARC下调用retain、release、autorelease、dealloc这些内存管理方法是非法的。
实际上,ARC在调用这些方法时,直接调用的是其底层C语言版本,如ARC会调用与retain等价的底层函数objc_retain,这样做性能更好,这也是不能重写retain、release、autorelease的原因。
ARC确立了方法名的硬性规定。若方法名以alloc、new、copy、mutableCopy开头,其返回对象归调用者所有。其他方法返回的对象并不归调用者所有。
+ (EOCPerson*)newPerson{
EOCPerson *person = [[EOCPerson alloc] init];
return person;
// 方法以new开头,返回值归调用者所有,释放由调用者负责,ARC不会在这里自动添加语句
}
+ (EOCPerson*)somePerson{
EOCPerson *person = [[EOCPerson alloc] init];
return person;
// 返回值不归调用者所有,ARC会自动将return person替换为等价return [person autorelease]的语句
}
- (void)doSomething{
EOCPerson *person1 = [EPCPerson newPerson];
EOCPerson *person2 = [EPCPerson somePerson];
// ARC会自动添加等价[person1 release]的语句(person1是由这块代码所有)
}
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong // 默认语义,保留此值
__unsafe_unreatined // 不保留此值,可能不安全,出现悬挂指针
__weak // 不保留此值,安全,系统回收对象时会清空对象
__autorelease // 把对象按引用传递给方法时,使用该修饰符。此值在方法返回时自动释放。
在手动管理引用计数时,可能会像下面这样来编写dealloc方法清空实例变量
- (void)dealloc{
[_foo release];
[_bar release];
[super dealloc];
}
用了ARC之后,就不需要再编写这样的dealloc方法了,ARC会自动清理内存。不过,如果有非Objective-C对像(如CoreFoundation)中的对象或是由malloc()分配在堆中的内存,仍然需要清理。在ARC中不能直接调用dealloc,但是可以重写dealloc方法,ARC会自动运行此方法,并调用其中超类的dealloc方法。
- (void)dealloc{
// 释放非Objective-C对象
CFRelease(_coreFoundationObject);
// 释放malloc()分配的堆内存
free(_heapAllocatedMemoryBlob);
}
第31条:在dealloc方法中只释放引用并解除监听
对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc方法了。在每个对象的生命周期内,此方法仅执行一次,也就是当保留计数降为0的时候。dealloc方法会由运行期系统调用,开发者不能自己调用。
ARC会自动释放所有Objective-C对象,dealloc中需要手动释放非Objective-C对象,除此之外,还需要把原来配置过的观测行为都清理掉。如果用NSNotificationCenter给此对象注册过某种通知,那么一般应该在这里注销。不然通知系统可能会把通知发送给已回收的对象,引起系统崩溃。
- (void)dealloc{
CFRelease(_coreFoundationObject);
// 注销通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
虽说应该于dealloc中释放引用,但是开销较大或系统内稀缺资源不在此列。如文件描述符、套接字、大块内存,不然会导致保留稀缺资源时间过长。通常应该实现另一个方法,但应用程序用完资源后就调用此方法清理资源。再在dealloc中进行检查,防止开发者忘记清理资源。
// 清理资源的方法
- (void)close{
/* 清理资源 */
_closed = YES;
}
- (void)dealloc{
// 如果忘记清理资源,则输出错误日志并清理资源
if(!_closed){
NSLog(@"ERROR:close was not called before dealloc!");
[self close];
}
}
本例中,在dealloc调用了其它方法,不过是为了侦测编程错误而破例。正常情况下,不要在dealloc中随便调用其他方法。因为对象已经处于正在回收状态,如果在这里调用的方法又要异步执行某些任务,等任务结束后这个对象可能已经被彻底摧毁了,导致程序崩溃。
dealloc里也不要调用属性的存取方法,属性可能正处于KVO机制的监控之下,属性的观察者可能会在属性值改变时保留或使用这个即将回收的对象,导致错误。
第32条:编写异常安全代码时留意内存管理问题
Objective-C的错误模型表明,异常只应在发生严重错误后抛出,不过有时候仍然需要编写代码来捕获并处理异常。
使用手动计数时:
@try{
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch(...){
Nslog(@"There was an error!");
}
// 无论是否发生异常,@finally块中的代码都会执行
@finally{
[object release];
}
使用@finally块可以在发生异常时也能释放对象。
而ARC环境下,不能调用release,无法像手动管理那样将释放操作移到@finally块中。ARC又不会自动处理,这种情况下可以打开编译器的-fobjc-arc-exceptions标志来开启ARC生成安全处理异常所用的附加代码。只是这段代码会严重影响运行期的性能,即使不抛出异常。
所以一般来说,只有当应用程序必须因异常情况而终止时才应抛出异常,这时候应用程序即将终止,是否发生内存泄漏已经无关紧要了,因此不用添加安全处理异常所用的附加代码了。如果有大量异常捕获操作时,应考虑重构代码,用21条的NSError式错误信息传递法来取代异常。