Effective Objective-C 学习第三周

时间:2024-02-21 20:17:48

理解引用计数

Objective-C 使用引用计数来管理内存:每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数:用完了之后,就递减其计数。计数变为 0时,就可以把它销毁。
在ARC中,所有与引用计数有关的方法都无法编译(由于 ARC 会在编译时自动插入内存管理代码,因此在编译时,所有与引用计数相关的方法都会被 ARC 替换为适当的代码)。

引用计数的工作原理

在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在 Objective-C 中叫做 “保留计数”也可以叫 “引用计数”。NSObject 协议声明了下面三个方法用于操作计数器,以递增或递减其值:

  • Retain 递增保留计数。
  • release 递减保留计数。
  • autorelease 待稍后清理 “自动释放池”时,再递减保留计数。
    查看保留计数的方法叫做 retainCount,不推荐使用。

对象创建出来时,其保留计数至少为 1。若想令其继续存活,则调用 retain 方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用 release 或 autorelease 方法。最终当保留计数归零时,对象就回收了,也就是说系统会将其占用的内存标记为 “可重用”。此时,所有指向该对象的引用也都变得无效。
应用程序在其生命周期中会创建很多对象,这些对象都相互联系着。例如,表示个人信息的对象会引用另一个表示人名的字符串对象,还可能会引用其他个人信息对象,比如在存放朋友的 set 中就是如此。这些相互关联的对象就构成了一张 “对象图”。对象如果持有指向其他对象的强引用,那么前者就 “拥有” 后者。对象想令其所引用的那些对象继续存活,就可将其 “保留”。等用完了之后,再释放。
按 “引用树” 回溯,那么最终会发现一个 “根对象”。在 iOS 应用程序中,它是 UIApplication 对象。是应用程序启动时创建的单例。
如下面这段代码:

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

//不能假设number对象一定存活
NSLog(@"number = %@", number);//***
//因为对象所占的内存在 “解除分配”之后,只是放回 “可用内存池”。如果执行 NSLog 时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。
//因过早释放对象而导致的 bug 很难调试。

[array release];

在上面这段代码中:创建了一个可变数组 array,然后创建了一个 NSNumber 对象 number,并将其添加到数组中,数组也会在 number 上调用retain 方法,以期继续保留此对象,这时number的引用计数至少为2。接着,我们试着通过 release 方法释放了 number 对象,因为数组对象还在引用着number对象,因此它仍然存活,但是不应该假设它一定存活。最后,通过调用 release 方法释放了数组 array,确保了内存的正确管理。上面这段代码在ARC中是无法编译的,因为调用了release方法。
调用者通过 alloc 方法表达了想令该对象继续存活下去的意愿,不过并不是说对象此时的保留计数必定是1。在 alloc 或 “initWithInt:” 方法的实现代码中,也许还有其他对象也保留了此对象(如初始化过程中的委托调用、通知其他对等),所以,其保留计数可能会大于 1。

为避免在不经意间使用了无效对象,一般调用完 release 之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为 “悬挂指针”。
可以这样编写代码来防止此情况发生:


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

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

刚才那个例子中的数组通过在其元素上调用 retain 方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问 “属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为 “strong 关系”,则设置的属性值会保留。假设有个名叫 foo 的属性由名为 _foo 的实例变量所实现,那么,该属性的设置方法会是这样:

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

当设置属性 foo 的新值时,属性的设置方法会依次执行以下操作:

  1. 保留新值:使用 retain 方法保留新值,确保其在设置方法之外仍然可用。
  2. 释放旧值:使用 release 方法释放旧值,以减少其保留计数,并在不再需要时释放内存。
  3. 更新实例变量:将实例变量 _foo 的引用指向新值。
    上面这些操作的顺序很重要。如果在保留新值之前就释放了旧值,并且旧值和新值指向同一个对象,那么在释放旧值时可能会导致对象被系统回收,而在后续保留新值时,该对象已经不存在了。这就会导致 _foo 成为一个悬挂指针,即指向已经释放的内存空间,这样的指针是无效的,并且可能导致应用程序崩溃或出现其他问题。

自动释放池

自动释放池(autorelease)是 Objective-C 内存管理的重要特性之一。
简单来说它允许开发者推迟对象的释放时间,通常在下一个事件循环中才执行释放操作。
比如说有这个代码:

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

在上面这段代码中,str对象在stringValue方法的作用域中被alloc出来,因此它的引用计数为1,但是因为它的作用域是stringValue方法,因此我们期望的是它在该方法结束前引用计数应为0,但是因为缺少了释放操作,因此该str对象的引用计数为1比期望值多1。
但是我们又不能直接在stringValue方法中将str对象释放,否则还没等方法返回,系统就把该对象回收了。这里应该用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越 “方法调用边界”(method callboundary)后一定存活。

  • 因此我们需要改写 stringValue 方法,使用 autorelease 来释放对象:
- (NSString *)stringValue {
    NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
    return [str autorelease];
}

改写后使用了 autorelease 方法将str对象放入自动释放池中,以延迟其释放时间。这样,在方法返回时,对象的引用计数会保持预期的值,而不会多出 1。
可以像下面这样使用 stringValue 方法返回的字符串对象:

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

在第一段代码中,NSString *str = [self stringValue]; 返回的字符串对象 str 是一个被放入自动释放池的对象。因此,尽管没有显式地调用 retain 方法,但在 NSLog(@“The string is: %@”, str); 之后,str 对象的引用计数不会被减少。这是因为该对象在自动释放池中,直到下一个事件循环才会被释放。
但是,如果你需要在稍后持有这个对象,比如将它设置给一个实例变量,那么你需要手动增加其引用计数,以防止在自动释放池释放时对象被释放,比如像这样,假设在另一个地方创建了一个 ExampleClass 的实例,并希望将stringValue返回的对象设置为该实例的 instanceVariable:

ExampleClass *exampleObject = [[ExampleClass alloc] init];
NSString *str = [exampleObject stringValue];

exampleObject.instanceVariable = str;

NSLog(@"The instance variable is: %@", exampleObject.instanceVariable);

则需要确保 str 对象不会在自动释放池被释放时被释放。因此,我们需要手动增加 str 对象的引用计数,以确保它不会被过早释放:

//手动增加引用计数
exampleObject.instanceVariable = [str retain];
//......

//并且在稍后手动释放
[exampleObject.instanceVariable release];

保留环

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

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

以ARC简化引用计数

引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。所以 Clang 编译器项目带有一个 “静态分析器”。用于指明程序里引用计数出问题的地方
比如再拿上一条中的这段代码举例子:

  if ([self shouldLogMessage]) {
      NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
      NSLog(@“str = %@, str);
  }

这段代码中,alloc增加了str对象的引用计数,但是在这个if块的作用域中,它却缺少了释放操作,因此str对象的引用计数比预期值多1,导致了内存泄漏。因为上述这些规则很容易表述,所以计算机可以简单地将其套用在程序上,从而分析出有内存泄漏问题的对象。这正是 “静态分析器” 要做的事。

静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题。自动引用计数的思路就是源于此。
因此假如使用了ARC,它就会自动将代码改写为这样:

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

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

  • retain
  • release
  • autorelease
  • dealloc
    直接调用上述任何方法都会产生编译错误,因为 ARC 要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。
    实际上,ARC 在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层 C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。

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

ARC 将内存管理语义在方法名中表示出来确立为硬性规定。
简单地体现在方法名上。若方法名以下列词语开头,则其调用上述四种方法的那段代码要负责释放方法所返回的对象:

  • alloc
  • new
  • copy
  • mutableCopy
    若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。 在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
    (我自己简单理解就是使用 “alloc”、“new”、“copy” 或者 “mutableCopy” 开头的方法,方法内部的引用计数要自己手动管理(release或者autorelease等),而不使用这四个开头的方法的引用计数就是自动管理的)。

除了会自动调用 “保留” 与 “释放” 方法外,使用 ARC 还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化,例如,在编译器,ARC 会把能够互相抵消的retain、release、autorelease 操作约简。如果发现在同一对象上执行了多次 “保留” 与 “释放” 操作,那么 ARC 有时可以成对地移除这两个操作。
ARC 也包含运行期组件。此时所执行的优化很有意义,大家看到之后就会明白为何以后的代码都应该用 ARC 来写了。前面讲到,某些方法在返回对象前,为其执行了 autorelease 操作,而调用方法的代码可能需要将返回的对象保留,比如像下面这种情况就是如此:

_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 和 retain 操作,提升代码的执行效率。
具体来说,在方法中返回自动释放的对象时,编译器会替换对 autorelease 方法的调用,改为调用 objc_autoreleaseReturnValue 函数。这个函数会检查方法返回后即将执行的代码,如果发现需要在返回的对象上执行 retain 操作,那么就会设置一个标志位,而不会立即执行 autorelease 操作。类似地,如果调用方法的代码需要保留返回的自动释放对象,那么编译器会将 retain 操作替换为 objc_retainAutoreleasedReturnValue 函数,该函数会检查之前设置的标志位,如果已经设置,则不会执行 retain 操作。
objc_autoreleaseReturnValue 函数检测方法调用者是否会立刻保留对象要根据处理器来定。

变量的内存管理语义

ARC 也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。
在编写设置方法(setter)时,使用 ARC 会简单一些。如果不用 ARC ,那么需要像下面这样来写:

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

但是在这段代码中,如果新值和实例变量已有的值相同,那么在执行设置方法时会出现问题。具体来说,当新值和旧值相同时,首先会调用 [_object release] 来释放旧值,此时如果旧值只有当前对象在引用,那么旧值的引用计数会减少为0。接着,会调用 [object retain] 来保留新值,但是此时旧值的内存已经被释放掉了,再次对其执行保留操作就会导致访问已释放的内存,从而引发应用程序崩溃。
使用 ARC 之后,就不可能发生这种疏失了。在 ARC 环境下,与刚才等效的设置函数可以这么写:

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

ARC 会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。用了 ARC 之后,根本无须考虑这种 “边界情况”。

  • 在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

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

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

@interface EOCClass : NSObject {
    __weak id _weakObject;
    __unsafe_unretained id _unsafeUnretainedObject;
}
@end

我们经常会给局部变量加上修饰符,用以打破由“块”,所引入的“保留环”。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致 “保留环”。可以用 __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);
}];

在这段代码中,我们使用了 __weak 修饰符来声明一个局部变量 weakFetcher,它指向 fetcher 对象。通过使用 __weak 修饰符,我们避免了块对 fetcher 对象的强引用,从而打破了潜在的保留环。

ARC 如何清理实例变量

ARC 也负责对实例变量进行内存管理。要管理其内存,ARC 就必须在 “回收分配给对象的内存”(deallocate)(也称为 “释放/回收/解除分配(内存)”) 时生成必要的清理代码。凡是具备强引用的变量,都必须释放,ARC 会在 dealloc 方法中插入这些代码。当手动管理引用计数时可以这样自己来编写 dealloc 方法:

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

这段代码做了以下几件事情:

  1. 释放 _foo 实例变量:调用 release 方法来减少对 _foo 对象的引用计数,如果引用计数为0,则会释放 _foo 对象所占用的内存。
  2. 释放 _bar 实例变量:同样地,调用 release 方法来减少对 _bar 对象的引用计数,如果引用计数为0,则会释放 _bar 对象所占用的内存。
  3. 调用 super 的 dealloc 方法:调用父类的 dealloc 方法来执行一些必要的清理工作,确保对象的内存被正确释放。
    使用 ARC 之后,不需要再编写这种 dealloc 方法。不过,如果有非 Objective-C 的对象,比如 CoreFoundation 中的对象或是由 malloc() 分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过,在 ARC 下不能直接调用 dealloc。ARC 会自动在 .cxx_destruct 方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的 dealloc 方法。ARC 环境下,dealloc 方法可以像这样写:
- (void)dealloc {
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}

可以使用CFRelease() 函数来释放CoreFoundation 对象。
可以使用 free() 函数来释放通过 malloc() 函数分配在堆上的内存块_heapAllocatedMemoryBlob

覆写内存管理方法

不使用 ARC 时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写 release 方法,将其替换为 “空操作”(no-op)。但在 ARC 环境下不能这么做,因为会干扰到 ARC 分析对象生命期的工作。而且,由于开发者不可调用及覆写这些方法,所以 ARC 能够优化 retain、release、autorelease 操作,使之不经过 Objective-C 的消息派发机制。优化后的操作,直接调用隐藏在运行期程序中的 C 函数。

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

在dealloc方法中只释放引用并解除监听

对象在经历其生命期后,最终会为系统所回收,这时就要执行 dealloc 方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为 0 的时候。然而具体何时执行,则无法保证。也可以理解成: 我们能够通过人工观察保留操作与释放操作的位置。来预估此方法何时即将执行。但实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc 方法,运行期系统会在适当的时候调用它。而且,一旦调用过 dealloc 之后,对象就不再有效了,后续方法调用均是无效的。
在 dealloc 方法中主要就是释放对象所拥有的引用。对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放,因为它们是由纯C 的API 所生成的。
在 dealloc 方法中,通常还要做一件事,那就是把原来配置过低观测行为都清理掉,比如消息通知的回收。
delloc应该这样写:

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

如果手动管理引用计数而不使用 ARC 的话,那么最后还需调用 “[super dealloc]”。ARC 会自动执行此操作。
开销较大或系统内部稀缺的资源不应该于 dealloc 中释放引用。像是文件描述符、套接字、大块内存等,都属于这种资源。不能指望 dealloc 方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。
比方说,如果某对象管理着连接服务器所用的套接字,那么也许就需要这种 “清理方法”。此对象可能要通过套接字连接到数据库。对于对象所属的类,其接口可以这样写:

#import <Foundation/Foundation.h>

@interface EOCServerConnection : NSObject

- (void)open:(NSString *)address;

- (void)close;

@end

这段代码提供了两个方法:

  • open: 方法:用于打开连接到服务器的套接字。它需要一个字符串类型的参数 address,表示服务器的地址。
  • close 方法:用于关闭当前打开的连接。
    这个类的设计允许用户通过 open: 方法打开连接,然后使用完成后通过 close 方法关闭连接
    在清理方法而非 dealloc 方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的 dealloc 都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到 dealloc 消息。由于应用程序终止之后,其占用的资源也会返还给操作系统,所以实际上这些对象也就等于是消亡了。不调用 dealloc 方法是为了优化程序效率。而这也说明系统未必会在每个对象上调用其 dealloc 方法。
    如果对象管理着某些资源,那么在 dealloc 中也要调用 “清理方法”,以防止开发者忘了清理这些资源。
    在系统回收对象之前,必须调用 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];
    }
}

编写 dealloc 方法时还需要注意,不要在里面随便调用其他方法。
调用dealloc 方法的那个线程会执行“最终的释放操作”,令对象的保留计数降为 0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在 dealloc 里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于 “正在回收的状态”,为了指明此状况,运行期系统已经改动了对象内部的数据结构。
在 dealloc 里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并与其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于 “键值观测” (KVO) 机制的监控之下,该属性的观察者(observer) 可能会在属性值改变时 “保留” 或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。

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

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

许多时下流行的编程语言都提供了 “异常”这一特性。在当前的运行期系统中,C++ 与 Objective-C 的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序”来捕获。
Objective-C 的错误模型表明,异常只应在发生严重错误后抛出,不过有时仍然需要编写代码来捕获并处理异常。比如使用 Objective-C++ 来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,比如,在使用 “键值观测”(KVO)功能时,若想注销一个尚未注册的“观察者”,便会抛出异常。
在 try 块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非 catch 块能处理此问题,否则对象所占内存就将泄漏。
异常处理例程将自动销毁对象,然而在手动管理引用计数时,销毁工作有些麻烦。以下面这段使用手工引用计数的 Objective-C 代码为例:

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

这段代码使用了 Objective-C 中的异常处理机制,尝试执行一些可能会抛出异常的代码,并在发生异常时捕获并处理它。具体来说:
@try { … } @catch (…) { … } 是 Objective-C 中的异常处理语法。@try 块用于包含可能会抛出异常的代码,而 @catch 块则用于捕获异常并进行处理。
在 @try 块中,首先创建了一个 EOCSomeClass 类的对象 object,然后调用了 doSomethingThatMayThrow 方法。这个方法可能会抛出异常。
在 @try 块的最后,调用了 [object release] 方法来释放 object 对象。这表明代码的编写者使用了手动内存管理。
如果在 @try 块中的代码抛出了异常,那么异常处理流程会跳转到对应的 @catch 块中。在这个例子中,@catch 块中的代码会执行,它打印了一条错误日志。由于 @catch 块的参数是 …,表示捕获所有类型的异常,因此无论什么类型的异常都会被捕获并处理。
但如果 doSomethingThatMayThrow 抛出异常了呢?由于异常会令执行过程终止并跳至catch 块,因而其后的那行 release 代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄漏了。这么做不好。解决方法是使用 @finally 块,无论是否抛出异常,其中代码都保证会运行,且只运行一次。比方说,刚才那段代码可改写如下:

EOCSomeClass *object;

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

在 ARC 环境下,问题会更严重。下面这段使用 ARC 的代码与修改前的那段代码等效:

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

在 ARC 下,由于不能手动调用 release 方法来释放对象,因此无法像在手动管理内存时那样将释放操作放在 @finally 块中。而且,ARC 不会自动处理异常导致的内存释放,因为这需要添加大量的额外代码来跟踪待清理的对象,并在抛出异常时释放它们,这可能会影响程序的性能并增加应用程序的大小。

虽然在 Objective-C 代码中,抛出异常通常是在应用程序必须因异常状况而终止时才发生的,但默认情况下 ARC 并不会为异常处理添加额外的代码。这是因为在应用程序即将终止时,是否会发生内存泄漏已经无关紧要了。因此,默认情况下,ARC 不会为异常处理添加额外的代码。如果需要在 ARC 环境下处理异常并进行自动内存管理,可以通过开启 -fobjc-arc-exceptions 编译器标志来实现。
但最重要的是:在发现大量异常捕获操作时,应考虑重构代码。

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

以弱引用避免保留环

对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成“环”。这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得它们都能继续存活下去,而不会为系统所回收。最简单的保留环由两个对象构成,它们互相引用对方。
例如这里有两个类:

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject

@property (nonatomic, strong) EOCClassB *other;

@end

@interface EOCClassB : NSObject

@property (nonatomic, strong) EOCClassA *other;

@end

保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。
避免保留环的最佳方式就是弱引用。这种引用经常用来表示 “非拥有关系”。将属性声明为 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 一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。
还可以使用weak 属性特质,刚刚的代码还可以修改为:

@property (nonatomic,weak) EOCClassA *other;

unsafe_unretained 与 weak 属性,在其所指的对象回收以后表现出来的行为不同。当指向 EOCClassA 实例的引用移除后,unsafe_unretained 属性仍然指向那个已经回收的实例,而 weak 属性则指向 nil。
一般来说,如果不拥有某对象,那就不要保留它,当然 collection 例外。有时,对象中的引用会指向另外一个并不归自己拥有的对象,比如 Delegate 模式就是这样。

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

以“自动释放池块”降低内存峰值

由前面的内容知道:自动释放池用于存放那些需要稍后某个时刻释放的对象。
创建自动释放池所用语法如下:

@autorelease {
    /...
}

通常只有一个地方需要创建自动释放池,那就是在 main 函数里。比如说iOS程序的main函数一般这样写:

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

从技术角度看,不是非得有个“自动释放池块”才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。这个池可以理解成最外围捕捉全部自动释放对象所用的池。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:

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

这段代码创建了两个自动释放池。第一个自动释放池从第 1 行开始,到第 10 行结束。在此范围内,字符串 string 被创建,并且在自动释放池的末尾被释放。
在第 5 行到第 8 行之间的范围内,又创建了一个新的自动释放池。在这个内层自动释放池中,数字 number 被创建,然后在内层自动释放池的末尾被释放。
注意,内层自动释放池的范围是嵌套在外层自动释放池的范围内的。因此,number 对象的释放操作会在外层自动释放池的范围结束时执行,而 string 对象的释放操作则会在整个自动释放池的范围结束时执行。

有如下代码:

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]