17.2 引用计数

谈到基本的Objective-C对象类NSObject时,注意到可以使用alloc方法进行内存分配,并且随后可以使用release消息来释放分配的内存。遗憾的是,事情并不总是这么简单。例如,正在运行的应用程序可以在多个位置引用你创建的对象;该对象可能存储在一个数组中,也可能被其他位置的实例变量引用。只有在确定使用该对象的每个人都使用完之后,才能释放它占用的内存。

幸运的是,Foundation框架提供了一个巧妙的解决方案,用于跟踪对象的引用次数。它涉及一个相当简单直观的技术,称为引用计数。其概念如下:创建对象时,将它的引用次数设置为1。每一次必须保持该对象时,就发送一条retain消息,使其引用次数加1,如下所示:


[myFraction retain];


Foundation框架提供的其他一些方法也可以增加对象的引用次数,例如,把对象添加到数组中时。

不再需要对象时,可以通过发送release消息,使对象的引用次数减1,如下所示:


[myFraction release];


当对象的引用次数达到0时,系统就知道不再需要这个对象(因为在理论上它不再被引用),因此系统就会释放(deallocates)它的内存。这是通过向对象发送一条dealloc消息而实现的。

对于程序员而言,要成功地运用这个策略需要足够的勤勉,以保证在程序运行期间正确地增减对象的引用次数。你将看到,系统会自动处理一些对象的引用次数,但并不是对所有对象都是如此。

了解一下引用计数的更多细节。通过向对象发送retainCount消息,可以获得这个对象的引用(或者保持)计数。通常情况下,永远也不会需要使用这个方法,但在这里,它对于说明问题非常有用(参见代码清单17-1)。注意,它返回一个NSUInteger类型的无符号整数。

代码清单17-1


//Introduction to reference counting

import<Foundation/NSObject.h>

import<Foundation/NSAutoreleasePool.h>

import<Foundation/NSString.h>

import<Foundation/NSArray.h>

import<Foundation/NSValue.h>

int main(int argc, char*argv[])

{

NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];

NSNumber*myInt=[NSNumber numberWithInteger:100];

NSNumber*myInt2;

NSMutableArray*myArr=[NSMutableArray array];

NSLog(@“myInt retain count=%lx”,

(unsigned long)[myInt retainCount]);

[myArr addObject:myInt];

NSLog(@“after adding to array=%lx”,

(unsigned long)[myInt retainCount]);

myInt2=myInt;

NSLog(@“after asssignment to myInt2=%lx”,

(unsigned long)[myInt retainCount]);

[myInt retain];

NSLog(@“myInt after retain=%lx”,

(unsigned long)[myInt retainCount]);

NSLog(@“myInt2 after retain=%lx”,

(unsigned long)[myInt2 retainCount]);

[myInt release];

NSLog(@“after release=%lx”,

(unsigned long)[myInt retainCount]);

[myArr removeObjectAtIndex:0];

NSLog(@“after removal from array=%lx”,

(unsigned long)[myInt retainCount]);

[pool drain];

return 0;

}


代码清单17-1输出


myInt retain count=1

after adding to array=2

after asssignment to myInt2=2

myInt after retain=3

myInt2 after retain=3

after release=2

after removal from array=1


NSNumber对象myInt被设置为整数100,并且程序输出显示它的初始保持计数为1。然后,使用addObject:方法将该对象加到数组myArr中。注意引用次数随之变为2。这是通过addObject:方法自动完成的。如果检查addObject:方法的文档,将发现这里说明的事实。将对象添加到任何类型的集合都会使该对象的引用次数增加。这意味着,如果随后释放了添加的对象,那么数组中仍然存在着该对象的有效引用,并且对象不会被释放。

接下来,将myInt赋值给变量myInt2。要注意这个操作并没有使myInt对象的引用次数增加,这可能会在以后造成潜在的麻烦。例如,如果对myInt的引用次数减少到0,并且它占用的空间被释放,那么myInt2将拥有无效的对象引用(请记住,将myInt赋值给myInt2的操作并没有复制实际的对象,只是指向myInt在内存中位置的指针)。

现在,因为myInt对象有另一个引用(通过myInt2),所以可以通过发送retain消息来增加它的引用次数。在代码清单17-1中使用过这个方法。可以看到,发送retain消息之后,它的引用数变为3。第一个引用是实际的对象本身,第二个是数组中的引用,第三个来自赋值引用。虽然将元素存储到数组会使对象的引用次数自动增加,但把它赋值给另一个变量时,引用次数不会自动增加,因此必须自己增加对象的引用次数。注意程序输出中myInt和myInt2的引用次数都是3,这是因为它们引用的是内存中的同一个对象。

假设在程序中已经使用完myInt对象。通过向其发送一条release消息,可以告知系统。可以看到,对象的引用次数从3降为2。因为引用次数并不为0,所以对象的其他引用(从数组和通过myInt2)仍然有效。只要对象具有非零的引用次数,系统就不会释放对象使用的内存。

如果使用removeObjectAtIndex:方法来删除数组myArr中的第一个元素,那么你将注意到对象myInt的引用值自动减为1。一般而言,从任何集合中删除对象都能够使其引用计数减小。这意味着下面的代码序列


myInt=[myArr ObjectAtIndex:0];

……

[myArr removeObjectAtIndex:0]

……


可能会产生麻烦。这是因为,在这种情况下,如果在调用removeObjectAtIndex:方法之后,对象的引用值减为0,那么myInt引用的对象将成为无效的。当然,此处的解决方案是从数组检索该值后,保持myInt,这样不管其他位置这个引用发生了什么事情,都不会产生任何影响。

17.2.1 引用计数和字符串

代码清单17-2演示了如何对string对象应用引用计数。

代码清单17-2


//Reference counting with string objects

import<Foundation/NSObject.h>

import<Foundation/NSAutoreleasePool.h>

import<Foundation/NSString.h>

import<Foundation/NSArray.h>

int main(int argc, char*argv[])

{

NSAutoreleasePool*pool=[[NSAutoreleasePool alloc]init];

NSString*myStr1=@“Constant string”;

NSString*myStr2=[NSString stringWithString:@“string 2”];

NSMutableString*myStr3=[NSMutableString stringWithString:@“string 3”];

NSMutableArray*myArr=[NSMutableArray array];

NSLog(@“Retain count:myStr1:%lx, myStr2:%lx, myStr3:%lx”,

(unsigned long)[myStr1 retainCount],

(unsigned long)[myStr2 retainCount],

(unsigned long)[myStr3 retainCount]);

[myArr addObject:myStr1];

[myArr addObject:myStr2];

[myArr addObject:myStr3];

NSLog(@“Retain count:myStr1:%lx, myStr2:%lx, myStr3:%lx”,

(unsigned long)[myStr1 retainCount],

(unsigned long)[myStr2retainCount],

(unsigned long)[myStr3 retainCount]);

[myArr addObject:myStr1];

[myArr addObject:myStr2];

[myArr addObject:myStr3];

NSLog(@“Retain count:myStr1:%lx, myStr2:%lx, myStr3:%lx”,

(unsigned long)[myStr1 retainCount],

(unsigned long)[myStr2retainCount],

(unsigned long)[myStr3 retainCount]);

[myStr1 retain];

[myStr2 retain];

[myStr3 retain];

NSLog(@“Retain count:myStr1:%lx, myStr2:%lx, myStr3:%lx”,

(unsigned long)[myStr1 retainCount],

(unsigned long)[myStr2 retainCount],

(unsigned long)[myStr3 retainCount]);

//Bring the reference count of myStr3 back down to 2

[myStr3 release];

[pool drain];

return 0;

}


代码清单17-2输出


Retain count:myStr1:ffffffff, myStr2:ffffffff, myStr3:1

Retain count:myStr1:ffffffff, myStr2:ffffffff, myStr3:2

Retain count:myStr1:ffffffff, myStr2:ffffffff, myStr3:3


将NSString对象myStr1赋值为NSConstantString@“Constant string”。内存中常量字符串的空间分配与其他对象不同。它们没有引用计数机制,因为永远不能释放这些对象,这就是为什么向myStr1发送消息retainCount,而它返回0xffffffff的原因(实际上,在标准头文件<limits.h>中,值0xffffffff被定义为最大的无符号整数,或者UINT_MAX)。

注意显然,在某些系统上,为代码清单17-2中的常量字符串返回的保持值是0x7fffffff(不是0xffffffff),这是最大的无符号整数值,或INT_MAX。

注意,这也同样适用于使用常量字符串初始化的不可变字符串对象:这种对象也没有保持值,为myStr2显示的保持值可以证明这一点。

注意此处系统比较聪明,确定了不可变字符串对象是由常量字符串对象初始化的。Leopard发布之前,这种优化是没有的,mystr2会有一个保持值。

在语句


NSMutableString*myStr3=[NSMutableString stringWithString:@“string 3”];


中,变量myStr3被设置为常量字符串@“string 3”的副本。制作字符串副本的原因是向NSMutableString类发送了stringWithString:消息,表明该字符串的内容可能在程序执行期间发生变化。由于常量字符串的内容是不能改变的,所以系统不能将变量myStr3设为指向常量字符串@“string 3”,而myStr2是可以的。

所以字符串对象myStr3没有引用计数,从程序输出可以证实。通过将这些字符串添加到数组中,或者向它们发送retain消息,可以更改这些引用计数,通过最后两行NSLog调用可以得到证实。在创建这两个对象时,通过Foundation的stringWithString:方法将这两个对象添加到自动释放池。通过Foundation的array方法将数组myArr添加到池中。

在释放自动释放池之前,myStr3被释放一次。这导致它的引用计数减为2。随后,自动释放池的释放使得这些对象的引用计数减为0,这导致它被释放。这是如何发生的呢?释放自动释放池时,每次向它发送autorelease消息时,池中的每个对象都会收到一条release消息。由于在stringWithString:方法创建字符串对象myStr3时,将它们添加到自动释放池,因此它们都会收到一条release消息。这导致它们的引用计数减为1。当自动释放池中的数组被释放时,数组中的每个元素也被释放。因此,从池中释放myArr时,该数字的每个元素(包括myStr3)都将收到一条release消息。这使其引用计数减为0,随之这个对象将被释放。

必须小心地不要过度释放对象。在代码清单17-2中,如果在释放该池之前,myStr3的引用计数少于2,那么该池将包含一个无效对象的引用。然后,释放该池时,无效对象的引用非常可能导致程序以一条分段识别错误而异常终止。