第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

 

posted @ 2017-08-14 00:05  鳄鱼不怕牙医不怕  阅读(214)  评论(0编辑  收藏  举报