第36条:不要使用 retainCount
本条要点:(作者总结)
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”(absolute retain count)都无法反映对象生命期的全貌。
- 引入 ARC 之后,retainCount 方法就正式废止了,在 ARC 下调用该方法会导致编译器报错。
Objective-C 通过引用计数来管理内存(参见第29条)。每个对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。对象创建好之后,其保留计数大于 0。保留与释放操作分别会使该计数递增及递减。当计数变为 0 时,对象就为系统所回收并摧毁了。
NSObject 协议中定义了下列方法,用于查询对象当前的保留计数:
- (NSUInteger)retainCount;
然而 ARC 已经将此方法废弃了。实际上,如果在 ARC 中调用,编译器就会报错,这和在 ARC 中调用 retain、release、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。若在不启用 ARC 的环境下编程(说真的,还是在 ARC 下编程比较好),那么仍可调用此方法,而编译器不会报错。所以,还是必须讲清楚为何不应该使用此方法。
这个方法看上去似乎挺合理、挺有用的。它毕竟返回了保留计数,而此值对每个对象来说显然都很重要。但问题在于,保留计数的绝对值一般都与开发者所应留意的事情完全无关。即便只在调试时才能调用此方法,通常也还是无所助益的。
此方法之所以无用,其首要原因在于:它返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。因此,下面这种写法非常糟糕:
1 while ([object retainCount]) { 2 3 [object release]; 4 }
这种写法的第一个错误是: 它没考虑到后续的自动释放操作,只是不停地通过释放操作来降低保留计数,直至对象为系统所回收。假如此对象也在自动释放池里,那么稍后系统清空池子时还要把它再释放一次,而这将导致程序崩溃。
第二个错误在于:retainCount 可能永远不返回0,因为有时候系统会优化对象的释放行为,在保留计数还是1 的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。因此,保留计数可能永远都不会完全归零。所以说,这段代码就算有时正常运行,也多半是凭运气,而非理性判断。对象回收之后,如果 while 循环仍在运行,那么目前的运行期系统一般都会直接令应用程序崩溃。
从来都不需要编写这种代码。这段代码所要实现的操作,应该通过内存管理来解决。开发者在期望系统于某处回收对象时,应该确保没有尚未抵消的保留操作,也就是不要令保留计数大于期望值。在这种情况下,如果发现某对象的内存泄漏了,那么应该检查还有谁仍然保留这个对象,并查明其为何没有释放此对象。
读者可能还是想看一看保留计数的具体值,然而看过之后就会觉得奇怪了;它的值为何这么大呢?比方说,有下面这段代码:
1 NSString *string = @"Some string"; 2 3 NSLog(@"String retainCount = %lu", [string retainCount]); 4 5 6 7 NSNumber *numberI = @1; 8 9 NSLog(@"numberI retainCount = %lu", [numberI reatainCount]); 10 11 12 13 NSNumber *numberF = @3.141f; 14 15 NSLog(@"numberF retainCount = %lu", [numberF retainCount]);
在 64 位 Mac OS X 10.8.2 系统中,用 Clang 4.1 编译后,这段代码输出的消息如下:
1 string retainCount = 18446744073709551615 2 3 number retainCount = 9223372036854775807 4 5 number retainConnt = 1
第一个对象的保留计数是 2的64次方减1,第二个对象的保留计数是 2的63次方减1。由于二者皆为 “单例对象”(singleton object),所以其保留计数都很大。系统会尽可能把 NSString 实现成单例对象。如果字符串像本例所举的这样,是个编译期常量(compile-time constant),那么就可以这样来实现了。在这种情况下,编译器会把 NSString 对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建NSString 对象。NSNumber 也类似,它使用了一种叫做 “标签指针”(tagged pointer)的概念来标注特定类型的数值。这种做法不使用 NSNumber 对象,而是把与数值有关的全部消息都放在指针里面。运行期系统会在消息派发期间检测到这种标签指针,并对它执行相应的操作,使其行为看上去和真正的 NSNubmer 对象一样。这种优化只在某些场合使用,比如范例中浮点数对象就没有优化,所以其保留计数就是 1。
另外,像刚才所说的那种单例对象,其保留计数绝对不会变。这种对象的保留及释放操作都是“空操作”(no-op)。可以看到,即便两个单例对象之间,其保留计数也各不相同,系统对其保留计数的这种处理方式再一次表明:我们不应该总是依赖保留计数的具体值来编码。假如你根据NSNumber 对象的具体保留计数来增减其值,而系统却以标签指针来实现此对象,那么编出来的代码就错了。
那么,只为了调试而使用 retainCount 方法行不行呢?即便只为调试,此方法也不是很有用。由于对象可能处在自动释放池中,所以其保留计数未必如想象般精确。而且其他程序库也可能自行保留或释放对象,这都会扰乱保留计数的具体取值。看了具体的计数值之后,你可能还误以为是自己的代码修改了它,殊不知其实是由深埋在另外一个程序库中的某段代码所改的。以下列代码为例:
1 id object = [self createObject]; 2 3 [opaqueObject doSomethingWithObject:object]; 4 5 NSLog(@"retainCount = %lu", [object retainCount]);
object 的保留计数是多少呢?这个计数可以是任意值。“doSomethingWithObject:” 方法也许会将对象加到多个 collection 中,而这些 collection 均会保留此对象。这个方法还可能会多次保留并自动释放此对象,而其中某些自动释放操作要留待系统稍后清空自动释放池时才执行。因此,保留计数的实际值就不是那么有用了。
那到底何时才应该用 retainCount 呢?最佳答案是:绝对不要用,尤其考虑到苹果公司在引入 ARC 之后已正式将其废弃,就更不应该用了。
END