[iOS开发]——内存管理(effectiveOC2.0阅读笔记)


在OC这种面相对象的语言里,内存管理是个重要概念。要想用一门语言写出内存使用效率高而且又没有bug代码,就得掌握其内存管理模型的种种细节。
一旦理解了这些规则,你就会发现,其实OC的内存管理没那么复杂,而且有了“自动引用计数”(Automatic Reference Counting,ARC)之后,就变得更为简单了。ARC几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。

第29条:理解引用计数

OC语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完之后,就递减其计数。计数变为0,就表示没人关注此对象了,于是,就可以把它销毁。要想写出优秀的OC代码,必须完全理解此问题才行,即便打算用ARC来编码也是如此。
从Mac OS X 10.8开始,“垃圾收集器”(garbage collector)已经正式废弃了,以OC代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集。因此,掌握引用计数机制对于学好OC来说十分重要。Mac OS X程序已经不能再依赖垃圾收集器了,而iOS系统不支持此功能,将来也不会支持。
已经用过ARC的人可能会知道:所有与引用计数有关的方法都无法编译,然而现在先暂时忘掉这件事。那些方法确实无法用在ARC中,不过本条就是要从OC的角度讲解引用计数,而ARC实际上也是一种引用计数机制,所以,还是要谈谈这些在开启ARC功能时不能直接调用的方法。
引用计数工作原理:
在引用计数的架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在OC中叫做“保留计数”(retain count),不过也可以叫做“引用计数”(reference count)。NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:

  1. Retain 递增保留计数
  2. release 递减保留计数
  3. autorelease待稍后清理“自动释放池”(autorel饿ase pool)时,再递减保留计数。

查看保留计数的方法叫retainCount,此方法不太有用,即便在调试时也如此,所以并不推荐大家使用这个方法。
对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain方法。要是某部分代码不在使用此对象,不想令其继续存活,那就调用release或autorelease方法。最终当保留计数归零时,对象就回收了(deallocated)。也就是说,系统会将其占用的内存标记为“可重用”(reuse)。此时,所有指向该对象的引用也都变得无效了。
如图演示了对象自创造出来之后历经一次“保留”及两次“释放”操作的过程。
在这里插入图片描述

应用程序在其生命期中会创建很多对象,这些对象都相互联系着。例如,表示个人信息的对象会引用另一个表示人名的字符串对象,而且可能还会引用其他个人信息对象,比如在存放朋友的set中就是如此,于是,这些互相关联的对象就构成了一张“对象图”(object graph)。对象如果持有指向其他对象的强引用(strong reference),那么前者就“拥有”(own)后者。也就是说,对象想令其所引用的那些对象继续存活,就可将其“保留”。等用完了之后再释放。
在下图中,ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectB与ObjectC继续存活,而应用程序里又又另外一些对象想令那些对象继续存活。如果按“引用树”回溯,那么最终会发现一个“根对象”(root object)。在Mac OS X应用程序中,此对象就是NSApplication对象;而在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时所创建的单例。
下面这段代码有助于理解这些方法的用法:

NSMutableArray *array = [[NSMutableArray alloc] init];

NSNumber *number = [[NSNumber alloc] initWithInt:1337];

[array addObject:number];

[number release];
//do something with 'array'
[array release];

在这里插入图片描述

如前所述,由于代码中直接调用了release 方法,所以在ARC下无法编译。在Objective-C中,调用alloc 方法所返回的对象由调用者所拥有。也就是说,调用者已通过alloc 方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是1。在 alloc 或"initWithInt∶"方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于1。能够肯定的是:保留计数至少为1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了该计数还是递减了该计数。
创建完数组后,把 number 对象加入其中。调用数组的"addObject∶"方法时,数组也会在 number上调用retain方法,以期继续保留此对象。这时,保留计数至少为2。接下来,代码不再需要 number 对象了,于是将其释放。现在的保留计数至少为1。这样就不能照常使用number 变量了。调用release 之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道 number 对象在调用了release 之后仍然存活,因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码:

NSNumber *number = [[NSNumber alloc] initWithInt:1337];

[array addObject:number];

[number release];

NSLog(@"number = %@", number);

即便上述代码在本例中可以正常执行,也仍然不是个好办法。如果调用release之后,基于某些原因,其保留计数降至0,那么 number 对象所占内存也许会回收,这样的话,再调用NSLog 可能就将使程序崩溃了。笔者在这里只说"可能",而没说"一定",因为对象所占的内存在"解除分配"(deallocated)之后,只是放回"可用内存池"(avaiable pool)。如果执行NSLog 时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见因过早释放对象而导致的bug很难调试。
为避免在不经意间使用了无效对象,一般调用完 release 之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为"悬挂指针(dangling pointer)。比方说,可以这样编写代码来防止此情况发生∶

NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;

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

如前所述,对象图由互相关联的对象所构成。刚才那个例子中的数组通过在其元素上调用 retain 方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问"属性"(参见第6条)来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为"strong关系"(strong relationship).则设置的属性值会保留。比方说.有个名叫 foo 的属性由名为_foo 的实例变量所实现,那么,该属性的设置方法会是这样∶

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

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么,先执行的 release 操作就可能导致系统将此对象永久回收。而后续的 retain 操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针。

自动释放池

在 Objective-C的引用计数架构中,自动释放池是一项重要特性。调用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,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身就一定是1,它可能大于1,不过那取决于"initWithFormat∶"方法内的实现细节。你要考虑的是如何将多出来的这一次保留操作抵消掉。
但是,不能在方法内释放 str,否则还没等方法返回,系统就把该对象回收了。这里应该用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越"方法调用边界"(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池(参见第 34条)时执行,除非你有自己的自动释放池。否则这个时机指的就是当前线程的下一次事件循环。改写stringValue 方法,使用autorelease 来释放对象∶

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

修改之后,stringValue 方法把NSString 对象返回给调用者时,此对象必然存活。所以我们能够像下面这样使用它∶

NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);

由于返回的 str 对象将于稍后自动释放,所以多出来的那一次保留操作到时自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以NSLog 语句在使用str 对象前不需要手工执行保留操作。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留,并于稍后释放∶

_instanceVariable = [[self stringValue] retain];
//...
[_instanceVariable release];

由此可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

保留环

使用引用计数机制时,经常要注意的一个问题就是“保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄漏,因为循环中的对象其保留计数不会降为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。下图里的每个对象都引用了另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是1。
在垃圾收集环境中,通常将这种情况认定为"孤岛"(island of isolation)。此时,垃圾收集器会把三个对象全都回收走。而在 Objective-C的引用计数架构中,则享受不到这一便利。通常采用"弱引用"(weak reference,参见第33条)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄漏。

要点

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

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

引用计数这个概念相当容易理解(参见第29条)。需要执行保留与释放操作的地方也很容易就能看出来。所以Clang 编译器项目带有一个"静态分析器"(static analyzer),用于指明程序里引用计数出问题的地方。举个例子,假设下面这段代码采用手工方式管理引用计数∶

if ([self shouldLogMessage]) {
	NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
	NSLog(@"message = %@", message);
}

此代码有内存泄漏问题,因为if语句块末尾并未释放 message 对象。由于在 if语句之外无法引用message,所以此对象所占的内存泄漏了。判定内存是否泄漏所用的规则很简明:调用NSString的 alloc 方法所返回的那个 message 对象的保留计数比期望值要多1。然而却没有与之对应的释放操作来抵消。因为这些规则很容易表述,所以计算机可以简单地将其套用在程序上。 从而分析出有内存泄漏问题的对象。这正是"静态分析器"要做的事。
静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题,是不是这样子呢?自动引用计数这一思路正是源于此。自动引用计数所做的事情与其名称相符。就是白动管理引用计数。干是,在前面那段代码的if语句块结束之前,可以于 message 对象上自动执行 release 操作,也就是把代码自动改写为下列形式∶

if ([self shouldLogMessage]) {
	NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
	NSLog(@"message = %@", message);
	[message release];///< Added by ARC
}

使用 ARC 时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由 ARC 自动为你添加。稍后将会看到,除了为方法所返回的对象正确运用内存管理语义之外,ARC还有更多的功能。不过,ARC的那些功能都是基于核心的内存管理语义而构建的,这套标准语义贯穿于整个Objective-C语言。
由于 ARC会自动执行 retain、release、autorelease 等操作,所以直接在 ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法∶

  1. retain
  2. release
  3. autorelease
  4. dealloc

直接调用上述任何方法都会产生编译错误,因为 ARC要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。此时必须信赖 ARC,令其帮你正确处理内存管理事宜,而这会使那些惯于手动管理引用计数的开发者不太放心。
实际上,ARC在调用这些方法时,并不通过普通的 Objective-C消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。比方说,ARC会调用与retain 等价的底层函数 objc retain。这也是不能覆写retain、release 或 autorelease 的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的Objective-C 方法来指代与之相关的底层C语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。

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

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

  1. alloc
  2. new
  3. copy
  4. mutableCopy

归调用者所有的意思是;调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了 autorelease,那么保留计数的值可能比1大,这也是 retainCount方法不太有用的原因之一(参见第36条)。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
维系这些规则所需的全部内存管理事宜均由 ARC 自动处理,其中也包括在将要返回的对象上调用autorelease,下列代码演示了ARC的用法∶

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

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

- (void)doSomething {
	EOCPerson *personOne = [EOCPerson newPerson];
	EOCPerson *personTwo = [EOCPerson somePerson];
}

ARC通过命名约定将内存管理规则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像 Objective-C这样强调命名。但是,想成为优秀的 Objective-C程序员就必须适应这套理念。在编码过程中,ARC 能帮程序员做许多事情。
除了会自动调用"保留"与"释放"方法外,使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够互相抵消的 retain、release、autorelease 操作约简。如果发现在同一个对象上执行了多次"保留"与"释放"操作,那么 ARC有时可以成对地移除这两个操作。
ARC也包含运行期组件。此时所执行的优化很有意义,大家看过之后就会明白为何以后的代码都应该用ARC来写了。前面讲到,某些方法在返回对象前,为其执行了autorelease 操作,而调用方法的代码可能需要将返回的对象保留,比如像下面这种情况就是如此∶

//From a class where _myPerson is a strong instancec variable
_myPerson = [EOCPerson personWithName:@"Bob Smith"];

调用"personWithName∶"方法会返回新的 EOCPerson 对象,而此方法在返回对象之前,为其调用了 autorelease 方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理引用计数的代码等效∶

EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = [tmp retain];

此时应该能看出来,“personWithName∶"方法里的 autorelease 与上段代码中的 retain 都是多余的。为提升性能,可将二者删去。但是,在 ARC环境下编译代码时,必须考虑"向后兼容性”(backward compatibility),以兼容那些不使用ARC的代码。其实本来 ARC也可以直接舍弃 autorelease 这个概念,并且规定,所有从方法中返回的对象其保留计数都比期望值多1。但是,这样做就破坏了向后兼容性。
不过,ARC可以在运行期检测到这一对多余的操作,也就是 autorelease 及紧跟其后的retain。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的 autorelease 方法,而是改为调用 objc autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码要在返回的对象上执行retain 操作,则设置全局数据结构(此数据结构的具体内容因处理器而异)中的一个标志位,而不执行 autorelease 操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain,而是改为执行 obic retainAutoreleasedReturnValue 函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行 retain 操作。设置并检测标志位,要比调用 autorelease 和 retain 更快。
下面这段代码演示了ARC是如何通过这些特殊函数来优化程序的∶

+ (EOCPerson *)personWithName:(NSString *)name {
	EOCPerson *person = [[EOCPerson alloc] init];
	person.name = name;
	objc_autoreleaseReturnValue(person);
}

EOCPerson *tmp = [EOCPerson personWithName:@"Matt Galloway"];
_myPerson = objc_retainAutoreleasedReturnValue(tmp);

为了求得最佳效率,这些特殊函数的实现代码都因处理器而异。下面这段伪代码描述了其中的步骤∶

id objc_autoreleaseReturnValue(id object) {
	if (/* caller will retain object */) {
		set_flag(object);
	} else {
		return [object autorelease];
	}
}

id objc_retainAutoreleasedReturnValue(id object) {
	if (get_flag(object)) {
		clear_flag(object);
		return object;///< No retain
	} else {
		return [object retain];
	}
}

objc autoreleaseReturnValue 函数究竟如何检测方法调用者是否会立刻保留对象呢?这要根据处理器来定。由于必须查看原始的机器码指令方可判断出这一点,所以,只有编译器的作者才能实现此函数。要想判断出方法调用者会不会保留方法所返回的对象,先得把调用方法的那段代码编排好才行,而这项任务只能由编译器的开发者来完成。
将内存管理交由编译器和运行期组件来做。可以使代码得到多种优化。 上面所进的只是其中种。我们由此应该可以了解到 ARC所带来的好处。待编译器与运行期组件日臻成熟,笔者相信还会出现其他优化技术。

变量的内存管理语义

ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。一定要理解这个问题,尤其要注意实例变量的语义,因为对于某些代码来说,其语义和手动管理引用计数时不同。例如,有下面这段代码:

@interface EOCClass : NSObject {
	id _object;
}

@implementation EOCClass
- (void) setup {
	_object = [EOCOtherClass new];
}
@end

在手动管理引用计数时,实例变量 object并不会自动保留其值,而在 ARC环境下则会这样做。也就是说,若在 ARC下编译 setup 方法,则其代码会变为∶

- (void) setup {
	id tmp = [EOCOtherClass new];
	_object = [tmp retain];
	[tmp release];
}

当然,在此情况下,retain和 release 可以消去。所以,ARC会将这两个操作化简掉,于是,实际执行的代码还是和原来一样。不过,在编写设置方法(setter)时,使用ARC会简单一些。如果不用ARC,那么需要像下面这样来写∶

- (void) setObject:(id)object {
	[_object release];
	_object = [object retain];
}

但是这样写会出问题。假如新值和实例变量已有的值相同,会如何呢? 如果只有当前对象还在引用这个值,那么设置方法中的释放操作会使该值的保留计数降为0,从而导致系统将其回收。接下来再执行保留操作,就会令应用程序崩溃。使用ARC之后,就不可能发生这种疏失了。在 ARC环境下,与刚才等效的设置函数可以这么写∶

- (void) setObject:(id)object {
	_object = object;
}

ARC会用一种安全的方式来设置;先保留新值,再释放旧值,最后设置实例变量。在手动管理引用计数时,你可能已经明白这个问题了,所以应该能正确编写设置方法,不过用了ARC之后,根本无须考虑这种"边界情况"(edge case)。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

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

比方说,想令实例变量的语义与不使用ARC时相同,可以运用weak或unsafe_unretained 修饰符∶

@interface EOCClass : NSObject {
	id _weak _weakObject;
	id unsafe unretained unsafeUnretainedObject;
}

不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版(Mac OS X 10.7、iOS 5.0及其后续版本)运行期程序库时,加了 weak修饰符的 weak引用才会自动清空,因为实现自动清空操作,要用到新版所添加的一些功能。
我们经常会给局部变量加上修饰符,用以打破由"块"(block,参见第 40条)所引入的"保留环"(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致"保留环"。可以用 weak 局部变量来打破这种"保留环"∶

NSURL*url = [NSURL URLWithString:@"http://www.example.com/"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
EOCNetworkFetcher * weak weakFetcher = fetcher;
[fetcher startWithCompletion:^(BOOL success){
	NSLog(@"Finished fetching from %@",weakFetcher.url);
}];

ARC如何清理实例变量

刚才说过,ARC也负责对实例变量进行内存管理。要管理其内存,ARC就必须在"回收分配给对象的内存’(deallocate)三时生成必要的清理代码(cleanup code)。凡是具备强引用的变量,都必须释放,ARC会在 dealloc方法中插入这些代码。当手动管理引用计数时,你可能会像下面这样自己来编写 dealloc 方法∶

- (void) dealloc {
	[_foo release];
	[_bar release];
	[super dealloc];
}

用了ARC之后,就不需要再编写这种 dealloc 方法了,因为 ARC会借用Objective-C++的一项特性来生成清理例程(cleanup routine)。回收 Objective-C++ 对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx destruct 的方法。而 ARC则借助此特性,在该方法中生成清理内存所需的代码。
不过,如果有非 Objective-C的对象,比如 CoreFoundation 中的对象或是由 malloc0分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过,在 ARC下不能直接调用dealloc。ARC会自动在.cxx destruct方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的 dealloc方法。ARC环境下,dealloc 方法可以像这样来写∶

-(void) dealloc(
	CFRelease ( coreFoundationObject);
	free ( heapAllocatedMemoryBlob);
}

因为 ARC会自动生成回收对象时所执行的代码,所以通常无须再编写dealloc 方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码(boilerplate code)。

覆盖内存管理方法

不使用ARC 时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写release 方法,将其替换为"空操作"(no-op)。但在 ARC环境下不能这么做,因为会干扰到ARC分析对象生命期的工作。而且,由于开发者不可调用及覆写这些方法,所以 ARC能够优化retain、release、autorelease 操作,使之不经过 Objective-C 的消息派发机制(参见第 11条)。优化后的操作,直接调用隐藏在运行期程序库中的C 函数。这就意味着 ARC可以执行各种优化了,比如刚才提到;如果方法命令即将返回的对象稍后"自动释放",而方法调用者立刻"保留"这个返回后的对象,那么这两个操作就会为 ARC 所化简。

要点

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

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

对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc 方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为0的时候。然而具体何时执行,则无法保证。也可以理解成;我们能够通过人工观察保留操作与释放操作的位置,来预估此方法何时即将执行。但实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc方法。运行期系统会在适当的时候调用它。而且,一旦调用过 dealloc 之后,对象就不再有效了,后续方法调用均是无效的。
那么,应该在 dealloc 方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把所有 Objective-C对象都释放掉,ARC会通过自动生成的.cxx destruct 方法(参见第30条),在 dealloc 中为你自动添加这些释放代码。对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放,因为它们是由纯 C 的 API所生成的。
在 dealloc方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior)都清理掉。如果用NSNotificationCenter给此对象订阅(register)过某种通知,那么一般应该在这里注销(unregister),这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。
dealloc 方法可以这样来写∶

-(void)dealloC {
	CFRelease (coreFoundationObject);
	[[NSNotificationCenter defaultCenter] removeObserver:self];
}

请注意,如果手动管理引用计数而不使用ARC的话,那么最后还需调用"[super dealloc]"。ARC会自动执行此操作,这再次表明其比手动管理更简单、更安全。若选择手动管理,则还要将当前对象所拥有的全部Objective-C 对象逐个释放。
虽说应该于 dealloc 中释放引用,但是开销较大或系统内稀缺的资源则不在此列。像是文件描述符(file descriptor)、套接字(socket)、大块内存等,都属于这种资源。不能指望dealloc 方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。在这种情况下,如果非要等到系统调用 dealloc方法时才释放,那么保留这些稀缺资源的时间就有些过长了,这么做不合适。通常的做法是,实现另外一个方法,当应用程序用完资源对象后,就调用此方法。这样一来,资源对象的生命期就变得更为明确了。
比方说,如果某对象管理着连接服务器所用的套接字,那么也许就需要这种"清理方法"(cleanup method)。此对象可能要通过套接字连接到数据库。对于对象所属的类,其接口可以这样写∶

import <Foundation/Foundation.h>
@interface EOCServerConnection :NSObject
- (void)open : (NSString *)address;
- (void)close;
@end

该类与开发者之间的约定是;想打开连接,就调用"open∶"方法;连接使用完毕,就调用 close 方法。“关闭"操作必须在系统把连接对象回收之前调用,否则就是编程错误(programmer error),这与通过"保留"及"释放"操作来平衡引用计数是类似的。
在清理方法而非 dealloc 方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的 dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到 dealloc 消息。由于应用程序终止之后,其占用的资源也会返还给操作系统,所以实际上这些对象也就等于是消亡了。不调用dealloc 方法是为了优化程序效率。而这也说明系统未必会在每个对象上调用其 dealloc 方法。在 Mac OS X及iOS 应用程序所对应的application delegate 中,都含有一个会于程序终止时调用的方法。如果一定要清理某些对象,那么可在此方法中调用那些对象的"清理方法”。
在 Mac OS X系统里,应用程序终止时会调用NSApplicationDelegate 之中的下述方法∶

- (void)applicationWillTerminate:(NSNotification *)notification

而在 iOS系统里,应用程序终止时则会调用 UIApplicationDelegate 之中的下述方法∶

- (void)applicationWillTerminate:(UIApplication *) application

如果对象管理着某些资源,那么在 dealloc 中也要调用"清理方法",以防开发者忘了清理这些资源。忘记清理资源的情况经常会发生,所以最好能输出一行消息,提示程序员代码里含有编程错误。在系统回收对象之前,必须调用close 以释放其资源,否则 close 方法就失去意义了,因此,没有适时调用close 方法就是编程错误。输出错误消息可促使开发者纠正此问题。而目,在程序员忘记调用close 的情况下。我们应该在 dealloc中补上这次调用。以防泄漏内存。下面举例说明 close 与dealloc 方法应如何来写∶

- (void)close [
	/* clean up resources */
	_closed = YES;}
- (void)dealloC {
	if(!_closed){
		NSLog(@"ERROR: close was not called before dealloc!");
		[self close];
	}
}	

有时可能不想只输出一条错误消息,而是要抛出异常来表明不调用close 方法是严重的编程错误。
编写dealloc 方法时还需注意,不要在里面随便调用其他方法。刚才那段范例代码中,dealloc方法确实调用了另外一个方法,不过那是为了侦测编程错误而破例。无论在这里调用什么方法都不太应该,因为对象此时"已近尾声"(in a winding-down state)。如果在这里所调用的方法又要异步执行某些任务,或是又要继续调用它们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。这会导致很多问题,且经常使应用程序崩溃,因为那些任务执行完毕后,要回调此对象,告诉该对象任务已完成,而此时如果对象已摧毁,那么回调操作就会出错。
请再注意一个问题;调用dealloc 方法的那个线程会执行"最终的释放操作"(final release),令对象的保留计数降为0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在 dealloc里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于"正在回收的状态"(deallocating state),为了指明此状况,运行期系统已经改动了对象内部的数据结构。
在 dealloc 里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于"键值观测"(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时"保留"或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。

要点

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

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

许多时下流行的编程语言都提供了"异常"(exception)这一特性。纯C中没有异常,而C++与Objective-C都支持异常。实际上,在当前的运行期系统中,C++与Objective-C的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编的"异常处理程序"(exception handler)来捕获。
Objective-C的错误模型表明,异常只应在发生严重错误后抛出(参见第 21条),虽说如此,不过有时仍然需要编写代码来捕获并处理异常。比如使用Objective-C++来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,这使我们想起从前那个频繁使用异常的年代。比如,在使用"键值观测"(KVO)功能时,若想注销一个尚未注册的"观察者",便会抛出异常。
发生异常时应该如何管理内存是个值得研究的问题。在 try 块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非 catch块能处理此问题,否则对象所占内存就将泄漏。C++的析构函数(destructor)由 Objective-C 的异常处理例程(exception-handle routine)来运行。这对于C++对象很重要,由于抛出异常会缩短其生命期,所以发生异常时必须析构,不然就会泄漏,而文件句柄(file handle)等系统资源因为没有正确清理,所以就更容易因此而泄漏了。
异常处理例程将自动销毁对象,然而在手动管理弓用计数时。销毁工作有些麻烦。以下面这段使用手工引用计数的 Objective-C代码为例∶

@try {
	EOCSomeClass *object = [ [EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow];
	[object release];
}
@catch(...) {
	NSLog(@"whoops,there was an error. Oh well...");
}

乍一看似乎没问题,但如果 doSomethingThatMayThrow抛出异常了呢?由于异常会令执行过程终止并跳至 catch 块,因而其后的那行 release 代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄漏了。这么做不好。解决办法是使用@finally 块,无论是否抛出异堂其中的代码都保证会运行,日 只运行一次。H方说,刚才那段代码可改写如下:

EOCSomeClass *object;
@try {
	object = [ [EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow] ;
}	
@catch (...){
	NSLog (@"whoops,there was an error.Oh well...");
}
@finally {
	[object release];
}

注意,由于@finally 块也要引用object 对象,所以必须把它从 @try 块里移到外面去。要是所有对象都得如此释放,那这样做就会非常乏味。而且,假如 @try 块中的逻辑更为复杂,含有多条语句,那么很容易就会因为忘记某个对象而导致泄漏。若泄漏的对象是文件描述符或数据库连接等稀缺资源(或是这些稀缺资源的管理者),则可能引发大问题,因为这将导致应用程序把所有系统资源都抓在自己手里而不及时释放。
在 ARC 环境下,问题会更严重。下面这段使用ARC 的代码与修改前的那段代码等效∶

@try {
	EOCSomeClass *object = [[EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow ];
}
@catch (...) {
	NSLog(@"whoops,there was an error. Oh well...");
}	

现在问题更大了∶由于不能调用release,所以无法像手动管理引用计数时那样把释放操作移到 @finally 块中。你可能认为这种状况 ARC自然会处理的。但实际上 ARC不会自动处理,因为这样做需要加入大量样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛异常时也如此。而且,添加进来的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。
虽说默认状况下未开启,但 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条:以弱引用避免保留环

对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成"环"(cycle)。由于 Objective-C 内存管理模型使用引用计数架构,所以这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得它们都能继续存活下去,而不会为系统所回收。
最简单的保留环由两个对象构成,它们互相引用对方。如图举例说明了这种情况。这种保留环的产生原因不难理解,且很容易就能通过查看代码而侦测出来∶
在这里插入图片描述

#import<Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@pzoperty(nonatomic,strong) EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property(nonatomic,strong) EOCClassA *other;
@end

看代码很容易就能发现其中可能出现的保留环∶如果把 EOCClassA实例的 other属性设置成某个EOCClassB实例,而把那个EOCClassB实例的 other属性又设置成这个EOCClassA 实例,那么就会出现下图的保留环:
在这里插入图片描述
保留环中只剩一个对象还为对象图里的其他对象所引用,移除此引用后整个保留环就泄漏了
保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。也就是说,没办法再访问其中的对象了。图 5-5所示的保留环更复杂一些,其中有四个对象,只有 ObjectB 还为外界所引用,把仅有的这个引用移除之后,四者所占内存就泄漏了。
Mac OS X平台的 Objective-C程序有个选项,可以启用垃圾收集器(garbage collector),它会检测保留环,若发现外界不再引用其中的对象,则将之回收。但是,从 Mac OS X 10.8 开始。垃圾收集机制就废弃了。而目iOS系统从未支持过这项功能。因此。从一开始编码时就要注意别出现保留环。
避免保留环的最佳方式就是弱引用。这种引用经常用来表示"非拥有关系"(nonowning relationship)。将属性声明为 unsafe_unretained 即可。修改刚才那段范例代码,将其属性声明如下∶

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end

@interface EOCClassB: NSObject
@property(nonatomic,unsafe unretained) EOCClassA *other;
@end

修改之后,EOCClassB 实例就不再通过 other 属性来拥有 EOCClassA实例了。属性特质(attribute)中的 unsafe unretained一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。
用 unsafe unretained修饰的属性特质,其语义同 assign特质等价(参见第6条)。然而,assign通常只用于"整体类型"(int、float、结构体等),unsafe unretained 则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用(unsafe)。
Objective-C中还有一项与ARC 相伴的运行期特性,可以令开发者安全使用弱引用∶这就是weak属性特质,它与unsafe unretained 的作用完全相同。然而,只要系统把属性回收,属性值就会自动设为 nil。在刚才那段代码中,EOCClassB的 other属性可修改如下∶
@property (nonatomic,weak) EOCClassA *other;
如图演示了 unsafe unretained与 weak 属性的区别:
在这里插入图片描述
当指向 EOCClassA 实例的引用移除后,unsafe unretained属性仍然指向那个已经回收的实例,而weak 属性则指向 nil。
但是,使用 weak属性并不是偷懒的借口。在刚才那个例子中,如果在 EOCClassA 对象已经回收之后,引用它的 EOCClassB实例仍然存活,那么就是编程错误。发生这种情况,就是 bug。开发者应确保程序中不出现此类问题。然而,使用 weak 而非 unsafe unretained引用可以令代码更安全。应用程序也许会显示出错误的数据,但不会直接崩溃。这么做显然比令终端用户直接看到程序退出要好。不过无论如何,只要在所指对象已经彻底销毁后还继续使用弱引用,那就依然是个bug。比方说,用户界面中的某个元素会把数据源设置给某个属性,并通过它来查询将要显示的数据。这种属性通常是弱引用(参见第 23条)。假如还未等界面元素查询完数据源对象就已经回收。那么。继续使用弱弓|用虽不致程序崩溃。但却无法再查到数据了。
一般来说,如果不拥有某对象,那就不要保留它。这条规则对 collection 例外,collection 虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。有时,对象中的引用会指向另外一个并不归自己所拥有的对象,比如 Delegate 模式就是这样(参见第 23条)。

要点

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

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

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

@autoreleasepool {
	// ...
}

如果在没有创建自动释放池的情况下给对象发送 autorelease 消息,那么控制台会输出这样一条信息∶

Object 0xabcd0123 of class NSCFString autoreleased with no pool in place -just leaking - break on objc_ autoreleaseNoPool () to debug

然而,一般情况下无须担心自动释放池的创建问题。Mac OS X与 iOS 应用程序分别运行于Cocoa 及 Cocoa Touch 环境中。系统会自动创建一些线程,比如说主线程或是"大中枢派发"(Grand Central Dispatch,GCD)③机制中的线程,这些线程默认都有自动释放池,每次执行"事件循环"(event loop)时,就会将其清空。因此,不需要自己来创建"自动释放池块"。通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们用自动释放池来包裹应用程序的主入口点((main application entry point)。比方说,iOS程序的 main 函数经常这样写∶

int main(int argc,char *argv[]) {
	@autoreleasepool {
		return UIApplicationMain (argc, argV, ni1, @"EOCAppDelegate");
	}
}	

从技术角度看,不是非得有个"自动释放池块"才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由 UIApplicationMain 函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并干对应的右花括号处自动清空。位于自动释放池范围内的对象。将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的油里。比方说∶

@autoreleasepool {
	NSString *string = [NSString stringwithFormat:@"1 = %i",1];
	@autoreleasepool {
		NSNumber *number = [NSNumber numberWithInt:1];
	}	
}

本例中有两个对象,它们都由类的工厂方法所创建,这样创建出来的对象会自动释放(参见第30条)。NSString 对象放在外围的自动释放池中,而 NSNumber 对象则放在里层的自动释放池中。将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
考虑下面这段代码∶

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

如果"doSomethingWithInt∶"方法要创建临时对象,那么这些对象很可能会放在自动释放池里。比方说,它们可能是一些临时字符串。但是,即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。这就意味着在执行for 循环时。会持续有新对象创建出来。并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来,在执行 for 循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
这种情况不甚理想,尤其当循环长度无法预知,必须取决于用户输入时更是如此。比方说,要从数据库中读出许多对象。代码可能会这么写∶

NSArray*databaseRecords =/* ...*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
	EOCPerson *person = [ [EOCPerson alloc] initWithRecord: record];
	[people addObject:person];
}

EOCPerson的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在"自动释放池块"中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如∶

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords)[
	@autoreleasepool {
		EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
		[people addObject:person];
	}	
}

加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量(highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。
自动释放池机制就像"栈"(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步。那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
如果在ARC 出现之前就写过Obiective-C程序.那么可能还记得有种老式写法。就是使用NSAutoreleasePool对象。这个特殊的对象与普通对象不同,它专门用来表示自动释放池,就像新语法中的自动释放池块一样。但是这种写法并不会在每次执行 for循环时都清空池,此对象更为"重量级"(heayyweight),通常用来创建那种偶尔需要清空的池。比方说∶

NSArray *databaseRecords = /* ... */;
NSMutableArray *people =[NSMutableArray new];
int i = 0;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (NSDictionary*record in databaseRecords) { 
	EOCPerson *person = [[EOCPerson alloc] initWithRecord: record];
	[people addObject:person];
	// Drain the pool only every 10 cycles 
	if (++i == 10){
		[pool drain];
		i= 0;
	}	
}
[pool drain];

现在不需要再这样写代码了。采用随着 ARC 所引入的新语法,可以创建出更为"轻量级"(lightweight)的自动释放池。原来所写的代码可能会每执行n 次循环清空一次自动释放池,现在可以改用自动释放池块把 for 循环中的语句包起来,这样的话,每次执行循环时都会建立并清空自动释放池。
@autoreleasepool语法还有个好处;每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。比方说,考虑下面这段采用旧式写法的代码∶

NSAutoreleasePool *pool =[ [NSAutoreleasePool alloc] init];
id object = [self createObject];
[pool drain];
[self useObject:object];

这样写虽然稍显夸张,但却能说明问题。调用"useObject∶"方法时所传入的那个对象,可能已经为系统所回收了。同样的代码改用新式写法就变成了∶

@autoreleasepool {
	id object = [self createObject];
}
[self useObject:object];

这次根本就无法编译,因为 object 变量出了自动释放池块的外围后就不可用了,所以在调用"useObject∶"方法时不能用它做参数。

要点

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

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

调试内存管理问题很令人头疼。大家都知道,向业已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里。而此对象也许能应答。也许不能。如果能。那程序就不崩溃,可你会觉得奇怪∶为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。
所幸 Cocoa提供了"僵尸对象"(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的"僵尸对象"、而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
将NSZombieEnabled环境变量设为 YES,即可开启此功能。比方说,在 Mac OS X系统中用 bash运行应用程序时,可以这么做∶

export NSZombieEnabled="YES"
./app

给僵尸对象发消息后。控制台会打印消息,而应用程序则会终止。打印出来的消息就像这样∶

*** -[CFString respondsToSelector:]: message sent to deallocated instance 0x7ff9e9c080e0

也可以在 Xcode里打开此选项,这样的话,Xcode 在运行应用程序时会自动设置环境变量。开启方法为∶编辑应用程序的 Scheme,在对话框左侧选择"Run",然后切换至"Diagnostics"分页,最后勾选"Enable Zombie Objects"选项。
那么,僵尸对象的工作原理是什么呢?它的实现代码深植于 Objective-C的运行期程序库、Foundation 框架及 CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。
下列代码有助于理解这一步所执行的操作:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface EOCClass: NSObject 
@end
@implementation EOCClass 
@end

void PrintClassInfo(id obj) {
	Class cls = object getClass (obj);
	Class superCls = class_getSuperclass(cls);
	NSLog(@"=== %s ∶%s ===", class_getName(cls),class_getName(superCls));
}
int main(int argc,char *argv[]) {
	EOCClass *obj= [[EOCClass alloc] init];
	NSLog(@"Before release:");
	PrintClassInfo(obj);
	
	[obj release];
	NSLog(@"After release:");
	PrintClassInfo(obj);
}

为了便于演示普通对象转化为僵尸对象的过程,这段代码采用了手动引用计数。因为假如使用ARC的话,str对象就会根据代码需要,尽可能多存活一段时间,于是在这个简单的例子中,就不可能变成僵尸对象了。这并不是说对象在 ARC下绝对不可能转化为僵尸对象。即便用了ARC,也依然会出现这种内存 bug,只不过一般要通过稍微复杂些的代码才能表现出来。
范例代码中有个函数,可以根据给定的对象打印出所属的类及其超类名称。此函数没有直接给对象发送Objective-C的 class 消息,而是调用了运行期库里的 object getClass()函数。因为如果参数已经是僵尸对象了,那么给其发送 Objective-C消息后,控制台会打印错误消息,而且应用程序会崩溃。范例代码将输出下面这种消息∶

Before release:
=== EOCClass :NSObject===
After release:
=== NSZombie EOCClass :nil ===

对象所属的类已由 EOCClass 变为 NSZombie EOCClass。但是,这个新类是从哪里来的呢?代码中没有定义过这样一个类。而且,在启用僵尸对象后,如果编译器每看到一种可能变成僵尸的对象,就创建一个与之对应的类,那也太低效了。NSZombie EOCClass 实际上是在运行期生成的,当首次碰到 EOCClass类的对象要变成僵尸对象时,就会创建这么一个类。创建过程中用到了运行期程序库里的函数,它们的功能很强大,可以操作类列表(class list)。
僵尸类(zombie class)是从名为 NSZombie 的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。接下来介绍它们是怎样充当标记的。首先来看下面这段伪代码,其中演示了系统如何根据需要创建出僵尸类。而僵尸类又如何把待回收的对象转化成僵尸对象。

// Obtain the class of the object being deallocated 
Class cls = object_getClass(self);
// Get the class's name
const char *clsName = class getName (cls);
// Prepend NSZombie to the class name
const char *zombieClsName =" NSZombie"+ clsName;
// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);
// If the specific zombie class doesn't exist,
// then it needs to be created 
if(!zombieCls)(
// Obtain the template zombie class called NSZombie 
Class baseZombieCls = objc_lookUpClass(" NSZombie_");
// Duplicate the base zombie class,where the new class's 
// name is the prepended string from above 
zombieCls = objc duplicateClass(baseZombieCls, zombieClsName, 0);
}
// Perform normal destruction of the object being deallocated 
objc destructInstance (self);
// set the class of the object being deallocated 
// to the zombie class
objc setClass(self,zombieCls);
// The class of'self'is now NSZombie OriginalClass

这个过程其实就是NSObject 的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把 dealloc方法"调配"(swizzle,参见第 13条)成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变为_NSZombie_OriginalClass 了,其中 OriginalClass 指的是原类名。
代码中的关键之处在于∶对象所占内存没有(通过调用 free(方法)释放,因此,这块内存不可复用。虽说内存泄漏了,但这只是个调试手段,制作正式发行的应用程序时不会把这项功能打开,所以这种泄漏问题无关紧要。
但是。系统为何要给每个变为僵尸的类都创建一个对应的新类呢?这是因为,给僵尸对象发消息后,系统可由此知道该对象原来所属的类。假如把所有僵尸对象都归到_NSZombie 类里,那原来的类名就丢了。创建新类的工作由运行期函数 objic duplicateClass()来完成,它会把整个 NSZombie 类结构拷贝一份,并赋予其新的名字。副本类的超类、实例变量及方法都和复制前相同。还有种做法也能保留旧类名,那就是不拷贝NSZombie,而是创建继承自 NSZombie 的新类,但是用相应的函数完成此功能,其效率不如直接拷贝高。
僵尸类的作用会在消息转发例程(参见第 12条)中体现出来。 NSZombie 类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和NSObject 一样,也是个"根类",该类只有一个实例变量,叫做 isa,所有Objective-C的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过"完整的消息转发机制"(full forwarding mechanism,参见第12条)。
在完整的消息转发机制中,forwarding是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为 NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息(本条目开头曾列出),其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。在僵尸类名中嵌入原始类名的好处,这时就可以看出来了。只要把 NSZombie 从僵尸类名的开头拿掉,剩下的就是原始类名。下列伪代码演示了这一过程:

// Obtain the object's class
Class cls = object_getClass(self);
// Get the class's name
const char *clsName = class getName (cls);
// Check if the class is prefixed with _NSZombie_
if(string has prefix(clsName," NSZombie_"){
// If so,this object is a zombie
// Get the original class name by skipping past the
// NSZombie,i.e. taking the substring from character 10 
const char *originalClsName = substring_from (clsName,10);
// Get the selector name of the message
const char*selectorName = sel getName ( cmd);
// Log a message to indicate which selector is 
// being sent to which zombie
Log("*** -[%s %s]:message sent to deallocated instance %p",
originalClsName,selectorName,self);
// Kill the application
abort();
}

把本节开头那个范例扩充一下,试着给变成僵尸的 EOCClass 对象发送 description消息:

EOCClass *obj=[[EOCClass alloc] init];NSLog (@"Before release:");
PrintClassInfo (obj);
[obj release];
NSLog (@"After release:");
PrintClassInfo (obj);
NSString *desc = [obj description];

若是开启了僵尸对象功能,那么控制台会输出下列消息∶

Before release:
=== EOCClass : NSObject ===After release:
=== NSZombie EOCClass : nil ===
***[EOCClass description];message sent to deallocated instance 0x7fc821c02a00

大家可以看到,这段消息明确指出了僵尸对象所收到的选择子及其原来所属的类,其中还包含接收消息的僵尸对象所对应的"指针值"(pointer value)。在调试器中深入分析程序时,也许会用到此消息。而且若能与适当的工具(比如 Xcode 自带的 Instruments)相搭配。则效果甚佳。

要点

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

第36条:不要使用retainCount

Objective-C通过引用计数来管理内存(参见第 29条)。每个对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。对象创建好之后,其保留计数大于0。保留与释放操作分别会使该计数递增及递减。当计数变为0时,对象就为系统所回收并摧毁了。
NSObject 协议中定义了下列方法,用于查询对象当前的保留计数∶

-(NSUInteger) retainCount

然而 ARC已经将此方法废弃了。实际上,如果在 ARC中调用,编译器就会报错,这和在 ARC中调用retain、release、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。若在不启用ARC的环境下编程(说真的,还是在ARC下编程比较好),那么仍可调用此方法,而编译器不会报错。所以,还是必须讲清楚为何不应使用此方法。
这个方法看上去似乎挺合理、挺有用的。它毕竟返回了保留计数,而此值对每个对象来说显然都很重要。但问题在于,保留计数的绝对数值一般都与开发者所应留意的事情完全无关。即便只在调试时才调用此方法,通常也还是无所助益的。
此方法之所以无用。其首要原因在干∶它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空(参见第 34条).因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。因此,下面这种写法非常糟糕∶

while([object retainCount]){
	[object release];
}

这种写法的第一个错误是∶它没考虑到后续的自动释放操作,只是不停地通过释放操作来降低保留计数,直至对象为系统所回收。假如此对象也在自动释放池里,那么稍后系统清空池子时还要把它再释放一次,而这将导致程序崩溃。
第二个错误在于;retainCount 可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。因此,保留计数可能永远都不会完全归零。所以说,这段代码就算有时能正常运行,也多半是凭运气,而非理性判断。对象回收之后,如果 while 循环仍在运行,那么目前的运行期系统一般会直接令应用程序崩溃。
从来都不需要编写这种代码。这段代码所要实现的操作,应该通过内存管理来解决。开发者在期望系统于某处回收对象时,应该确保没有尚未抵消的保留操作,也就是不要令保留计数大于期望值。在这种情况下,如果发现某对象的内存泄漏了,那么应该检查还有谁仍然保留这个对象,并查明其为何没有释放此对象。
读者可能还是想看一看保留计数的具体值,然而看过之后你就会觉得奇怪了;它的值为何那么大呢?比方说,有下面这段代码∶

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);
NSNumber *numberI = @1;
NSLog (@"numberI retainCount = %lu",[numberI retainCount]);
NSNumber *numberF = @3.141f;
NSLog (@"numberF retainCount = 8lu",[numberF retainCount]);

在64 位 Mac OS X10.8.2系统中,用Clang 4.1编译后,这段代码输出的消息如下∶

string retainCount = 18446744073709551615 
numberI retainCount = 9223372036854775807 
numberF retainCount = 1

第一个对象的保留计数是2°-1,第二个对象的保留计数是283-1。由于二者皆为"单例对象"(singleton object),所以其保留计数都很大。系统会尽可能把 NSString 实现成单例对象。如果字符串像本例所举的这样,是个编译期常量(compile-time constant),那么就可以这样来实现了。在这种情况下,编译器会把 NSString 对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建 NSString 对象。NSNumber 也类似,它使用了一种叫做"标签指针"(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber 对象。而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发(参见第11条)期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的 NSNumber 对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。
另外,像刚才所说的那种单例对象,其保留计数绝对不会变。这种对象的保留及释放操作都是"空操作"(no-op)。可以看到,即便两个单例对象之间,其保留计数也各不相同,系统对其保留计数的这种处理方式再-一次表明∶我们不应该总是依赖保留计数的具体值来编码。
假如你根据 NSNumber对象的具体保留计数来增减其值。而系统却以标签指针来实现此对象,那么编出来的代码就错了。
那么,只为了调试而使用 retainCount 方法行不行呢?即便只为调试,此方法也不是很有用。由于对象可能处在自动释放池中,所以其保留计数未必如想象般精确。而且其他程序库也有可能自行保留或释放对象,这都会扰乱保留计数的具体取值。看了具体的计数值之后,你可能还误以为是自己的代码修改了它,殊不知其实是由深埋在另外一个程序库中的某段代码所改的。以下列代码为例∶

id object =[self createObject];
[opaqueObject doSomethingWithObject:object];
NSLog (@"retainCount = 8lu",[object retainCount]);

object 的保留计数是多少呢?这个计数可以是任意值。"doSomethingWithObject∶"方法也许会将对象加到多个collection中,而这些 collection均会保留此对象。这个方法还可能会多次保留并自动释放此对象,而其中某些自动释放操作要留待系统稍后清空自动释放池时才执行。因此,保留计数的实际值就不是那么有用了。
那到底何时才应该用retainCount呢?最佳答案是∶绝对不要用,尤其考虑到苹果公司在引入 ARC之后已正式将其废弃,就更不应该用了。

要点

  1. 对象的保留计数看似有用,实则不然,因为任何给定时间点上的"绝对保留计数"都无法反映对象生命期的全貌。
  2. 引入 ARC之后,retainCount方法就正式废止了,在 ARC下调用该方法会导致编译器报错。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值