iOS开发读书笔记:Effective Objective-C 2.0 52个有效方法 - 篇3/4

iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇1/4
iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇2/4
iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇3/4
iOS开发读书笔记:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 - 篇4/4

  • 第五章 内存管理
    • 第29条:理解引用计数
    • 第30条:以ARC简化引用计数
    • 第31条:在dealloc方法中只是否引用并解除监听
    • 第32条:编写“异常安全代码”时留意内存管理问题
    • 第33条:以弱引用避免保留环
    • 第34条:以“自动释放池块”降低内存峰值
    • 第35条:用“僵尸对象”调试内存管理问题
    • 第36条:不要使用retainCount

第五章 内存管理

ARC几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。ARC实际上也是一种引用计数机制。

第29条:理解引用计数

Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。
从Mac OS X 10.8开始,“垃圾收集器”(garbage collector)已经正式废弃了,以Objective-C代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集器。

引用计数工作原理

对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用 release或autorelease方法。最终当保留计数归零时,对象就回收了(deallocated),NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:

  1. retain递增保留计数。
  2. release递减保留计数。
  3. autorelease待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数。(本条后面几页与本书第34条将会详细讲解“自动释放池”。)

注意:查看保留计数的方法叫做retainCount,此方法不太有用。

如果按“引用树”回溯,那么最终会发现一个“根对象”(root object)。在Mac OS X应用程序中,此对象就是NSApplication对象;而在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时创建的单例。

为避免在不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为“悬挂指针’(dangling pointer)。

属性存取方法中的内存管理

其他对象也可以保留别的对象,这一般通过访问“属性”(参见第6条)来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“strong关系”(strong relationship),则设置的属性值会保留。

-(void)setFoo:(id)foo {
  [foo retain];
  [_foo release];
  _foo = foo;
}

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。

自动释放池

调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用 autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减, 不过也可能执行得更早些(参见第34条)。
此特性很有用,尤其是在方法中返回对象时更应该用它。在这种情况下,我们并不总是想令方法调用者手工保留其值。比方说,有下面这个方法:

- (NSString *)stringValue {
  NSString *str = [[NSString alloc] initWithFormat: @"I am this: %@", self];
  return str;
}

此时返回的str对象其保留计数比期望值要多1 ( +1 retain count),因为调用alloc会令保留计数加1,而又没有与之对应的释放操作。保留计数多1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。

但是,不能在方法内释放str,否则还没等方法返回,系统就把该对象回收了。这里应该用autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。

换句话说,此方法可以保证对象在跨越“方法调用边界”(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池(参见第34条)时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。改写stringValue方法,使用autorelease来释放对象:

- (NSString*)stringValue {
  NSString *str = [[NSString alloc]  initWithFormat: @"I am this: %@", self];
  return [str autorelease];
}

修改之后,stringValue方法把NSString对象返回给凋用者时,此对象必然存活。
由于返回的str对象将于稍后自动释放,所以多出来的那一次保留操作到时自然就会抵消,无须再执行内存管理操作。

保留环

使用引用计数机制时,经常要注意的一个问题就是“保留环"(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄漏。
通常采用“弱引用”(weak reference,参见第33 条)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄漏。

要点:

  1. 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
  2. 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

第30条:以ARC简化引用计数

引用计数这个概念相当容易理解(参见第29条)。需要执行保留与释放操作的地方也很容易就能看出来。所以Clang编译器项目带有一个“静态分析器"(static analyzer),用于指明程序里引用计数出问题的地方。从而分析出有内存泄漏问题的对象。这正是“静态分析器”要做的事。静态分析器还有更为深入的用途。既然可以査明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题,对吧?自动引用计数这一思路正是源于此。

使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加。
由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法(retain、release、autorelease、dealloc)是非法的。

实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层C语言版本。这样做性能更好。比方说,ARC会调用与retain等价的底层函数objc_ retain。这也是不能覆写retain、release或autorelease的缘由,因为这些方法从来不会被直接调用。

使用ARC时必须遵循的方法命名规则

将内存管理语义在方法名中表示出来早已成为Objective-C的惯例,而ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以alloc、new、copy、mutableCopy词语开头,则其返回的对象归调用者所有。

归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。 也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。

若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。比如下面的代码:

+ (EOCPerson *)newPerson {
  EOCPerson *person = [[EOCPerson alloc] init]; 
  return person;
}

+ (EOCPerson*)somePerson {
  EOCPerson *person = [[EOCPerson alloc] init]; 
  return [person autorelease];
}

ARC通过命名约定将内存管理规则标准化,其他编程语言很少像Objective-C这样强调命名。

ARC的好处:
好处1:自动调用“保留”与“释放”方法;
好处2:它可以执行一些手工操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够互相抵消的retain、 release、autorelease操作约简。如果发现在同一个对象上执行了多次“保留”与“释放”操作, 那么ARC有时可以成对地移除这两个操作;
好处3:ARC也包含运行期组件。此时所执行的优化很有意义,前面讲到,某些方法在返回对象前,为其执行了 autorelease 操作,而调用方法的代码可能需要将返冋的对象保留,ARC可以在运行期检测到这一对多余的操作,为了优化代码,不直接调用对象的autorelease/retain方法,而是改为调用objc autoreleaseRetumValue/retainAutoreleasedRetumValue ,要比调用autorelease和retain更快。

将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化。

变量的内存管理语义

ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。ARC会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

  1. strong:默认语义,保留此值。
  2. __unsafe_unretained:不保留此值,这么做可能不安全,因为等到再次使用变量时, 其对象可能已经回收了。
  3. __weak:不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了, 那么变量也会自动清空。
  4. __autoreleasing :把对象“按引用传递”(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。

ARC如何清理实例变量

刚才说过,ARC也负责对实例变量进行内存管理。要管理其内存,ARC就必须在“回收分配给对象的内存’(deallocate) 时生成必要的淸理代码(cleanup code)。凡是具备强引用的变量,都必须释放,ARC会在dealloc方法中插入这些代码。

ARC会借用0bjective-C++的一项特性来生成清理例程(cleanup routine)。回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。而ARC则借助此特性,自动在该方法中生成清理内存所需的代码。(而在生成的代码中会自动调用超类的dealloc方法)

不过,如果有非Objective-C的对象,比如Core Foundation中的对象或是由malloc()分配在堆中的内存,那么仍然需要清理。ARC环境下,dealloc方法可以像这样来写:

- (void)dealloc {
  CFRelease(_coreFoundationObject); 
  free(_heapAllocatedMemoryBlob);
}

因为ARC会自动生成回收对象时所执行的代码,所以通常无须再编写dealloc方法。

覆写内存管理方法

由于开发者不可调用及覆写retain、release、autorelease方法,所以ARC能够优化这些方法,使之不经过Objective-C 的消息派发机制(参见第11条)。优化后的操作,直接调用隐藏在运行期程序库中的C函数。

要点:

  1. 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。
  2. ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。 在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
  3. 由方法所返回的对象,其内存管理语义总是通过方法名来体现ARC将此确定为开发者必须遵守的规则。
  4. ARC只负责管理Objective-C对象的内存。尤其要注意:Core Foundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。

第31条:在dealloc方法中只释放引用并解除监听

对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为0的时候。

那么,应该在dealloc方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把所有Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法(参见第30条),在dealloc中为你自动添加这些释放代码。对象所拥有的其他非Objective-C对象也要释放。 比如Core Foundation对象就必须手动释放,因为它们是由纯C的API所生成的。

在dealloc方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior)都清理掉。

在清理方法而非dealloc方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这 些对象没有收到dealloc消息。

编写dealloc方法时还需注意,不要在里面随便调用其他方法。如果在这里所调用的方法又要异步执行某些任务,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。

在dealloc里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于“键值观测”(Key-Value Observation, KV0)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时 “保留”或使用这个即将回收的对象。

要点:

  1. 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”〇CVO)或NSNotificationCenter等通知,不要做其他事情。
  2. 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
  3. 执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在冋收的状态了。

第32条:编写“异常安全代码”时留意内存管理问题

许多时下流行的编程语言都提供了 “异常"(exception)这一特性。纯C中没有异常,而 C++与Objective-C都支持异常。

@try {
  //...
}
@catch (. . . ) {
  //...
)
@finally {
  //...
}

Objective-C的错误模型表明,异常只应在发生严重错误后抛出(参见第21条),虽说如此,不过有时仍然需要编写代码来捕获并处理异常。比如编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,这使我们想起从前那个频繁使用异常的年代。比如,在使用“键值观测”(KVO)功能时,若想注销一个尚未注册的“观察者”,便会拋出异常。

发生异常时应该如何管理内存是个值得研究的问题。在try块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非catch块能处理此问题,否则对象所占内存就将泄漏。

在ARC下使用try导致的内存泄露:虽说默认状况下未开启,但ARC依然能生成这种安全处理异常所用的附加代码。-fobjc- arc-exceptions这个编译器标志用来开启此功能。其默认不开启的原因是:在Objective-C代码中,只有当应用程序必须因异常状况而终止时才应拋出异常(参见第21条)。因此,如果应用程序即将终止,那么是否还会发生内存泄漏就已经无关紧要了。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。

有种情况编译器会自动把-fobjc-arc-exceptions标志打开,就是处于Objective-C++模式时。因为C++处理异常所用的代码与ARC实现的附加代码类似,所以令ARC加入自己的代码以安全处理异常,其性能损失并不太大。此外,由于C++频繁使用异常,所以Objective-C++程序员很可能也会使用异常。

如果手工管理引用计数,而且必须捕获异常,那么要设法保证所编代码能把对象正确清理干净;若使用ARC且必须捕获异常,则需打开编译器的-fobjc-arc-exceptions标志。但最重要的是:在发现大量异常捕获操作时,应考虑重构代码,用第21条所讲的NSError式错误信息传递法来取代异常。

要点:

  1. 捕获异常时,一定要注意将try块内所创立的对象清理干净。
  2. 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

第33条:以弱引用避免保留环

由于Objective-C内存管理模型使用引用计数架构,所以这种情况通常会泄漏内存, 避免保留环的最佳方式就是弱引用。这种引用经常用来表示“非拥有关系”(ruHiowning relationship)。

__unsafe_unretained修饰的属性特质,其语义同assign特质等价(参见第6条)。然而,assign通常只用于“整体类型”(int、float、结构体等),__unsafe_unretained则多用于对象类型。 这个词本身就表明其所修饰的属性可能无法安全使用(unsafe)。

Objective-C中还有一项与ARC相伴的运行期特性,可以令开发者安全使用弱引用: 这就是weak属性特质,它与__unsafe_unretained的作用完全相同。然而,只要系统把属性回收,属性值就会自动设为nil。当引用移除后,__unsafe_unretained属性仍然指向那个已经回收的实例,而weak属性则指向nil。因此使用weak而非__unsafe_unretained引用可以令代码更安全。

要点:

  1. 将某些引用设为weak,可避免出现“保留环”。
  2. weak引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

第34条:以“自动释放池块”降低内存峰值

Objective-C对象的生命期取决于其引用计数(参见第29条)。在Objective-C的引用计数架构中,有一项特性叫做“自动释放池”(autoreleasepool)。释放对象有两种方式:一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入“自动释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动 释放池时,系统会向其中的对象发送release消息。
创建自动释放池所用语法如下:

@autoreleasepool {
  //...
}

系统会自动创建一些线程,比如说主线程或是“大中枢 派发’(Grand Center Dispatch,GCD)机制中的线程,这些线程默认都有自动释放池,每次执行“事件循环"(event loop)时,就会将其清空。因此,不需要自己来创建“自动释放池块”。 通常只有一个地方需要创建自动释放池,那就是在main函数里,我们用自动释放池来包裹应用程序的主入口点(main application entry point)。这个池可以理解成最外围捕捉全部自动释放对象所用的池。

自动释放池可以嵌套,将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
考虑下面这段代码:

for (int i = 0; i <100000; i++) {
  [self doSomethingWithInt:i];
}

如果“doSomethingWithInt:”方法要创建临时对象,那么这些对象很可能会放在自动释放池里。比方说,它们可能是一些临时字符串。但是,即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。这就意味着在执行for循环时,会持续有新对象创建出来,并加入自动释放池中。所有这种对象都要等for循环执行完才会释放。这样一来,在执行for循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。

增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在“自动释放池块”中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。

for (int i = 0; i <100000; i++) {
  @autoreleasepool {
    [self doSomethingWithlnt:i];
  }
}

加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。

内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量 (highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。

自动释放池机制就像“栈”(stack) —样。系统创建好自动释放池之后,就将其推入栈中, 而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。

是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。

如果在ARC出现之前就写过Objective-C程序,那么可能还记得有种老式写法,就是使用NSAutoreleasePool对象。这个特殊的对象与普通对象不同,它专门用来表示自动释放池,就像新语法中的自动释放池块一样。但是这种写法并不会在每次执行for循环时都清空池,此对象更为“重量级"(heavyweight),通常用来创建那种偶尔需要清空的池。

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

现在不需要再这样写代码了。采用随着ARC所引入的新语法,可以创建出更为“轻量级”(light weight)的自动释放池。原来所写的代码可能会每执行n次循环清空一次自动释放池,现在可以改用自动释放池块把for循环中的语句包起来,这样的话,每次执行循环时都会建立并清空自动释放池。

@autordeasepool语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。

要点:

  1. 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
  2. 合理运用自动释放池,可降低应用程序的内存峰值。
  3. @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。

第35条:用“僵尸对象”调试内存管理问题

调试内存管理问题很令人头疼。向业已回收的对象发送消息是不安全的。 这么做有时崩溃,有时不崩溃。崩溃与否,完全取决于对象所占内存有没有为其他内容所覆写。

所幸Cocoa提供了 “僵尸对象”(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。僵尸对象收到消息后,会拋出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。将NSZombieEnabled环境变量设为YES,即可开启此功能。

僵尸对象的工作原理:系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤,这一步就是把即将要变成僵尸对象所属的类,由EOCClass变为_NSZombie_EOCClass。但是,这个新类是从哪里来的呢?_NSZombie_EOCClass实际上是在运行期生成的,当首次碰到EOCClass类的对象要变成僵尸对象时,就会创建这么一个类。 创建过程中用到了运行期程序库里的函数。

系统根据需要创建出僵尸类之后,僵尸类又把待回收的对象转化成僵尸对象。这个过程其实就是NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把dealloc方法“调配”(swizzle,参见第13条) 成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变为_NSZombie_ OriginalClass了,其中OriginalClass指的是原类名。

僵尸类的作用会在消息转发例程(参见第12条)中体现出来。_NSZombie_类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和NSObject—样,也是个 “根类”,该类只有一个实例变量,叫做isa,所有Objective-C的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制” (full forwarding mechanism,参见第 12 条)。

在完整的消息转发机制中,forwarding是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检査接收消息的对象所属的类名。若名称前缀为NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。在僵尸类名中嵌入原始类名的好处,这时就可以看出来了。只要把_NSZombie_从僵尸类名的开头拿掉,剩下的就是原始类名。

要点:

  1. 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
  2. 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。 儸尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

第36条:不要使用retainCount

此方法之所以无用,其首要原因在于:它所返回的保留计数只是某个给定时间点上的值。 该方法并未考虑到系统会稍后把自动释放池清空(参见第34条),因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。

我们可能还是想看一看保留计数的具体值,然而看过之后你就会觉得奇怪了,它的值为何那么大呢?比方说,有下面这段代码:

NSString *string = @"Some string";
NSLog (@"string  retainCount = %lu", [string retainCount]);

NSNumber *numberl = @1;
NSLog (@"numberl retainCount = %lu", [numberl retainCount]);

NSNumber *numberF = @3.141f;
NSLog (@"numberF retainCount = %lu", [numberF retainCount]);

在64位MacOSX10.8.2系统中,用Clang4.1编译后,这段代码输出的消息如下:

string retainCount = 18446744073709551615
numberl retainCount = 9223372036854775807
numberF retainCount = 1

第一个对象的保留计数是2(64)-1,第二个对象的保留计数是2(63)-1。由于二者皆为“单例对象"(singleton object),所以其保留计数都很大。系统会尽可能把NSString实现成单例对象。 如果字符串像本例所举的这样,是个编译期常量(compile-time constant),那么就可以这样来实现了。在这种情况下,编译器会把NSString对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建NSString对象。NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发(参见第11条)期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumbei *对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。

要点:

  1. 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数” (absolute retain count)都无法反映对象生命期的全貌。
  2. 引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。