- 17.2.2 引用计数与实例变量
- import<Foundation/NSObject.h>
- import<Foundation/NSAutoreleasePool.h>
- import<Foundation/NSString.h>
- import<Foundation/NSObject.h>
- import<Foundation/NSAutoreleasePool.h>
- import<Foundation/NSString.h>
- import<Foundation/NSArray.h>
- import<Foundation/NSObject.h>
- import<Foundation/NSAutoreleasePool.h>
- import<Foundation/NSString.h>
- import<Foundation/NSArray.h>
17.2.2 引用计数与实例变量
处理实例变量时,必须注意它们的引用计数。例如,回想一下类AddressCard中的setName:方法:
-(void)setName:(NSString*)theName
{
[name release];
name=[[NSString alloc]initWithString:theName];
}
假设使用下面这种方式定义setName:方法,并且它没有name对象的所有权。
-(void)setName:(NSString*)theName
{
name=theName;
}
这个版本的setName:方法使用代表人名的字符串,并将它存储到name实例变量中。这看起来简单明了,然而考虑下面的方法调用:
NSString*newName;
……
[myCard setName:newName];
假设newName是一个临时存储空间,它存储要添加到地址卡上的人名,并且后来你要释放它。你认为myCard中的实例变量name将发生什么呢?因为它引用了已经销毁的对象,所以它的name域将不再有效。这就是你的类必须拥有自己的成员对象的原因:这样就不用担心无意中修改或释放这些对象。
下面几个例子更详细地说明了该点。首先定义一个新类ClassA,这个类有一个实例变量:字符串对象str。必须为这个变量编写取值方法和赋值方法。我们在此没有合并这些方法,然而我们自己写出了这些方法,这样可清楚地知道发生了什么。
代码清单17-3
//Introduction to reference counting
import<Foundation/NSObject.h>
import<Foundation/NSAutoreleasePool.h>
import<Foundation/NSString.h>
@interface ClassA:NSObject
{
NSString*str;
}
-(void)setStr:(NSString*)s;
-(NSString*)str;
@end
@implementation ClassA
-(void)setStr:(NSString*)s
{
str=s;
}
-(NSString*)str
{
return str;
}
@end
int main(int argc, char*argv[])
{
NSAutoreleas ePool*pool=[[NSAutoreleasePool alloc]init];
NSMutableString*myStr=[NSMutableStr ing stringWithString:@“A string”];
ClassA*myA=[[ClassA alloc]init];
NSLog(@“myStr retain count:%x”,[myStr retainCount]);
[myA setStr:myStr];
NSLog(@“myStr retain count:%x”,[myStr retainCount];
[myA release];
[pool drain];
return 0;
}
代码清单17-3输出
myStr retain count:1
myStr retain count:1
这个程序仅仅分配了一个ClassA对象myA,然后调用它的赋值方法,将其设置为myStr指定的NSString对象。和你期望的一样,在调用setStr方法前后,myStr的引用计数都为1,这是因为该方法只是将它的参数值存储到实例变量str中。然而,再次声明,如果程序在调用setStr方法之后释放myStr,那么存储在实例变量str中的值将变成无效的,这是因为它的引用计数将减为0,并且将释放它引用的对象所占用的内存空间。
在代码清单17-3中,当自动释放池被释放时,情况的确如此。即使我们没有显式地将对象添加到自动释放池,使用stringWithString:方法创建字符串对象myStr时,该对象也被添加到自动释放池中。所以,该池被释放时,myStr也被释放。因此,在池被释放后对它的任何访问都将无效。
代码清单17-4更改了setStr:方法,以保持str的值。这样,以后其他人释放str引用的对象时,你不会受到影响。
代码清单17-4
//Retaining objects
import<Foundation/NSObject.h>
import<Foundation/NSAutoreleasePool.h>
import<Foundation/NSString.h>
import<Foundation/NSArray.h>
@interface ClassA:NSObject
{
NSString*str;
}
-(void)setStr:(NSString*)s;
-(NSString*)str;
@end
@implementation ClassA
-(void)setStr:(NSString*)s
{
str=s;
[str retain];
}
-(NSString*)str
{
return str;
}
@end
int main(int argc, char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];
NSString*myStr=[NSMutableString stringWithString:@“A string”];
ClassA*myA=[[ClassA alloc]init];
NSLog(@“myStr retain count:%x”,[myStr retainCount]);
[myA setStr:myStr];
NSLog(@“myStr retain count:%x”,[myStr retainCount]);
[myStr release];
NSLog(@“myStr retain count:%x”,[myStr retainCount]);
[myA release];
[pool drain];
return 0;
}
代码清单17-4输出
myStr retain count:1
myStr retain count:2
myStr retain count:1
可以看到调用setStr:方法之后,myStr的引用值升为2。因此,这个问题就得到了解决。随后,释放程序中的myStr时,因为它的引用计数为1,所以通过这个实例变量的引用仍然有效。
因为在程序中使用alloc为myA分配了资源,所以你仍要负责释放它。要想不必自己释放它,可以通过向它发送一条autorelease消息,将它添加到自动释放池:
[myA autorelease];
如果想要,可以在给对象分配资源后立即将它添加到自动释放池中。记住,将对象添加到自动释放池中并没有释放它或使它无效;这仅仅是为以后释放它做了标记。当池被释放时,如果对象的引用值恰好变为0,则释放对象的资源。在此之前,可以继续使用这个对象。
你可能看出,代码清单17-4中还有一些潜在的问题。方法setStr:的作用是保持了作为参数传入的字符串对象,然而什么时候释放这个对象呢?还有,重写实例变量str时,原来的值如何?不应该释放它的值以收回它占用的内存吗?代码清单17-5提供了这些问题的解决方法。
代码清单17-5
//Introduction to reference counting
import<Foundation/NSObject.h>
import<Foundation/NSAutoreleasePool.h>
import<Foundation/NSString.h>
import<Foundation/NSArray.h>
@interface ClassA:NSObject
{
NSString*str;
}
-(void)setStr:(NSString*)s;
-(NSString*)str;
-(void)dealloc;
@end
@implementation ClassA
-(void)setStr:(NSString*)s
{
//free up old object since were done with it
[str autorelease];
//retain argument in case someone else releases it
str=[s retain];
}
-(NSString*)str
{
return str;
}
-(void)dealloc{
NSLog(@“ClassA dealloc”);
[str release];
[super dealloc];
}
@end
int main(int argc, char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];
NSString*myStr=[NSMutableString stringWithString:@“A string”];
ClassA*myA=[[ClassA alloc]init];
NSLog(@“myStr retain count:%x”,[myStr retainCount]);
[myA autorelease];
[myA setStr:myStr];
NSLog(@“myStr retain count:%x”,[myStr retainCount]);
[pool drain];
return 0;
}
代码清单17-5输出
myStr retain count:1
myStr retain count:2
ClassA dealloc
无论实例变量Str中当前存储的是什么,setStr:方法首先接受这个对象,并将它添加到自动释放池中。也就是,这使得以后可以自动释放str变量。如果在整个程序执行过程中多次调用setStr:方法,以便同一个域设置不同值,那么这项操作尤为重要。每次存储新值时,变量的旧值应该被标记为自动释放。释放旧的值之后,将保持新值并将其存储到str域中。消息表达式
str=[s retain];
利用了retain方法返回它的接收者这个事实。
注意如果变量str为nil,这没有问题。Objective-C运行时将所有变量都初始化为nil,向nil发送消息是没有问题的。
Dealloc方法并不是新出现的,在第15章“数字、字符串和集合”中使用类AddressBook和AddressCard时,曾经遇到过它。重载的dealloc方法为你提供了一种简洁的方式,用于在释放str实例变量时(也就是,当它的引用计数变为0时),处理该对象引用的最后一个对象。在这种情况下,系统调用dealloc方法,这个方法继承自NSObject,通常情况下,不需要重载这个方法。对于保持的对象,使用alloc方法分配的对象,或者在方法中复制(使用下一章将讨论的一个复制方法)的对象,可能需要重载dealloc方法,以便有机会释放它们。语句:
[str release];
[super dealloc];
首先释放str实例变量,然后调用父类的dealloc方法来完成这项工作。
调用dealloc方法时,该方法中的NSLog语句用来输出一条消息。这样做的目的仅仅是为了验证释放自动释放池时正确地释放了ClassA对象。
对于赋值方法setStr,你可能已经发现它最后的一个缺陷。再看一下代码清单17-5。假设myStr是可变字符串而不是不变字符串;进一步假设,在调用setStr方法之后,字符串myStr中的一个或多个字符更改了。更改myStr所引用的字符串,也会影响到其他引用此字符串的实例变量,因为它们实际上引用的是同一对象。重读最后一句,以确保已经理解了这点。并且,认识将myStr设置为全新的字符串对象,不会产生这个问题。仅当通过某种方式修改字符串中的一个或多个字符,才会出现这个问题。
如果想要保护字符串,并且使它完全独立于设置方法的参数,那么这个问题的解决方案就是在设置方法中生成字符串的全新副本。这就是在第1 5章中,为什么我们选择在类AddressCard的方法setName:和setEmail:方法中创建name和email成员的全新副本。