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

  本条要点:(作者总结)

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

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

1    if ([self shouldLogMessage]) {
2 
3     NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
4 
5     NSLog(@"Message = %@", message);
6 
7     } 

  此代码有内存泄漏问题,因为 if 语句块末尾并未释放 message 对象。由于在 if 语句之外无法引用 message,所以此对象所占的内存泄漏了(这里“泄漏”的意思是:没有正确释放已经不再使用的内存)。判定内存是否泄漏所用的规则很简明:调用NSString 的 alloc 方法所返回的那个 message 对象的保留计数比期望值要多 1。然而却没有与之对应的释放操作来抵消。因为这些规则很容易表述,所以计算机可以简单地将其套用在程序上,从而分析出有内存泄漏问题的对象。这正是 “静态分析器” 要做的事。

  静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题,对吧?自动引用计数这一思路正是源于此。自动引用计数所做的事情与其名称相符,就是自动管理引用计数。于是,在前面那段代码的 if 语句块结束之前,可以于 message 对象上自动执行 release 操作,也就是把代码自动改写为下列形式:

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

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

  由于 ARC 会自动执行 retain、release 、autorelease 等操作,所以直接在 ARC 下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:

  • retain
  • release
  • autorelease
  • dealloc

  直接调用上述任何方法都会产生编译错误,因为 ARC  要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。此时必须信赖 ARC,令其帮你正确处理内存管理事宜,而这会使那些惯于手动管理引用计数的开发者不太放心。

  实际上,ARC 在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层 C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。比方说,ARC 会调用与 retain 等价的底层函数 objc_retain。这也是不能覆写 retain、release 或 autorelease 的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的 Objective-C 方法来指代与之相关的底层 C 语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。

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

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

  • alloc
  • new
  • copy
  • mutableCopy

  归调用者所有的意思是: 调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了 autorelease,那么保留计数的值可能比 1 大,这也是 retainCount 方法不太有用的原因之一。

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

  维系这些规则所需的全部内存管理事宜均由 ARC 自动处理,其中也包括在将要返回的对象上调用 autorelease,下列代码演示了 ARC 的用法:

 1 + (EOCPerson *)newPerson {
 2 
 3   EOCPerson *person = [[EOCPerson alloc] init];
 4 
 5   return person;
 6   /**
 7 
 8    * The method name begins with 'new', and since 'person'
 9    * already has an unbalanced + 1 retain count from the
10 
11    * 'alloc', no retain, release, or autorelease are
12 
13    * required when returning.
14 
15    */
16 
17 }
 1 + (EOCPerson *)somePerson {
 2 
 3   EOCPerson *person = [[EOCPerson alloc] init];
 4 
 5   return person;
 6 
 7   /**
 8 
 9    * The method name does not begin with one of the "owning"
10 
11    * prefixes, therefore ARC will add an autorelease when
12 
13    * returning 'Person'
14 
15    * The equivalent manual reference counting statement is: 
16 
17    * return [person autorelease];
18 
19    */
20 
21 }
 1 - (void)doSomething {
 2 
 3 EOCPerson *personOne = [EOCPerson newPerson];
 4 
 5 // ...
 6 
 7 EOCPerson *personTwo = [EOCPerson somePerson];
 8 
 9 // ...
10 
11 /**
12 
13  * At the point, 'personOne' and 'personTwo' go out of scope, therefore ARC needs to clean them up as required. - 'personOne' was returned as owned by this blok of code, so it needs to be released.
14 
15 * - 'personTwo' was returned not owned by this block of code , so it does not needs to be released.
16 
17 * The equivalent manual reference counting cleanting cleanup code is: [PersonOne release];
18 
19 */
20 
21 }

   ARC 通过命名约定将内置管理准则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像 Objective-C 这样强调命名。但是,想成为优秀的 Objective-C 程序员就必须适应这套理念。在编码过程中,ARC 能帮助程序员做许多事情。

  除了会自动调用 “保留” 与 “释放” 方法外,使用 ARC 还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化,例如,在编译器,ARC 会把能够互相抵消的retain、release、autorelease 操作约简。如果发现在同一对象上执行了多次 “保留” 与 “释放” 操作,那么 ARC 有时可以成对地移除这两个操作。

  ARC 也包含运行期组件。此时所执行的优化很有意义,大家看到之后就会明白为何以后的代码都应该用 ARC 来写了。前面讲到,某些方法在返回对象前,为其执行了 autorelease 操作,而调用方法的代码可能需要将返回的对象保留,比如像下面这种情况就是如此:

1   // From a class where _myPerson is a strong instance variable
2 
3   _myPerson = [EOCPerson personWithName:@"Bob smith"];

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

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

  此时应该能看出来, “personWithName:” 方法里面的 autorelease 与上段代码中的 retain 都是多余的。为提升性能,可将二者删去。但是,在 ARC 环境下编译代码时,必须考虑 “向后兼容性”(backward compatibility),以兼容那些不使用 ARC 的代码。其实本来 ARC 也可以直接舍弃 autorelease 这个概念,并且规定,所有从方法中返回的对象其保留计数都比期望值多 1。但是,这样做就破坏了向后兼容性。

  不过,ARC 可以在运行期检测到这一对多余的操作,也就是 autorelease 及紧跟其后的 retain。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的 autorelease 方法,而是改为调用 objc_autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码要在返回的对象上执行 retain 操作,则设置全局数据结构(此数据结构的具体内容因处理器而异)中的一个标志位,而不执行 autorelease 操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain,而是改为执行 objc_retainAutoreleasedReturnValue 函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行 retain 操作。设置并检测标志位,要比调用 autorelease 和 retain 更快。

   下面这段代码演示了 ARC 是如何通过这些特殊函数来优化程序的:

 1 // Within EOCPerson class
 2 
 3 + (EOCPerson *)personWithName:(NSString *)name {
 4 
 5   EOCPerson *person = [[EOCPerson alloc] init];
 6 
 7   person.name = name;
 8 
 9   objc_autoreleaseReturnValue(person);
10 
11 }
1 // Code using EOCPerson class
2 
3 EOCPerson *tmp = [EOCPerson personWithName:@"Matt Galloway"];
4 
5 _myPerson = objc_retainAutoreleasedReturnValue(tmp);

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

 1   id objc_autoreleaseReturnValue(id object) {
 2 
 3   if (/* caller will retain object*/) {
 4 
 5     set_flag(object);
 6     return object; ///< No autorelease
 7   } else {
 8 
 9     return [object autorelease];
10 
11   }
12 
13   }
 1 id objc_retainAutoreleasedReturnValue (id object) {
 2 
 3     if (get_flag(object)) {
 4       clear_flag(object);
 5       return object; ///< No retain
 6       } else {
 7         return [object retain];
 8       }
 9 
10    } 

  objc_autoreleaseReturnValue 函数究竟如何检测方法调用者是否会立刻保留对象呢?这要根据处理器来定。由于必须查看原始的机器码指令方可判断出这一点,所以,只有编译器的作者才能实现此函数。要想判断出方法调用者会不会保留方法所返回的对象,先得把调用方法的那段代码编排好才行,而这项任务只能由编译器的开发者来完成。

  将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化,上面所讲的只是其中一种。我们由此应该可以了解到 ARC 所带来的好处。待编译器与运行期组件日臻成熟,笔者相信还会出现其他优化技术。

变量的内存管理语义

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

 1   @interface EOCClass : NSObject {
 2 
 3     id _object;
 4   }
 5 
 6   @implementation EOCClass 
 7 
 8   - (void)setup {
 9 
10     _object = [EOCOtherClass new];
11   }
12 
13   @end

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

1 - (void)setup {
2 
3   id tmp = [EOCOtherClass new];
4 
5   _object = [tmp retain];
6 
7   [tmp release];
8 
9 }

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

1 - (void)setObject:(id)object {
2 
3   [_object release];
4 
5   _object = [object retain];
6 
7 }

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

1 - (void)setObject:(id)object {
2 
3   _object = object;
4 
5 }

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

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

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

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

1   @interface EOCClass : NSObject {
2 
3   id _weak _weakObject;
4 
5   id _unsafe_unretained _unsafeUnretainedObject;
6 
7   }

  不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版 (Mac OS X 10.7、iOS 5.0 及其后续版本) 运行期程序库时,加了 __weak 修饰符的 weak 引用才会自动清空,因为实现自动清空操作,要用到新版所添加的一些功能。

  我们经常会给局部变量加上修饰符,用以打破由“块”(block),所引入的“保留环”(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致 “保留环”。可以用 __weak 局部变量来打破这种 “保留环”:

1   NSURL *url = [NSURL URLWithString:@"http://www.example.com/"];
2 
3   EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
4 
5   EOCNetworkFetcher *__weak weakFetcher  = fetcher;
6 
7   [fetcher startWithCompletion:^(BOOL success) {
8     NSLog(@"Finished fetching from %@", weakFetcher.url);
9   }];

ARC 如何清理实例变量

 

  刚才说过,ARC 也负责对实例变量进行内存管理。要管理其内存,ARC 就必须在 “回收分配给对象的内存”(deallocate)("deallocate" 也称为 “释放(内存)”,然而在 Objective-C 中,“释放” 一词与 “release”操作相对应,所以为了避免混淆,译文改用“回收”、“解除分配” 等说法来对应 “dealoocate”) 时生成必要的清理代码(cleanup code)。凡是具备强引用的变量,都必须释放,ARC 会在 dealloc 方法中插入这些代码。当手动管理引用计数时,你可能会像下面这样自己来编写 dealloc 方法:

1   - (void)dealloc {
2 
3   [_foo release];
4 
5   [_bar release];
6 
7   [super dealloc];
8 
9   }

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

  不过,如果有非 Objective-C 的对象,比如 CoreFoundation 中的对象或是由 malloc() 分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过,在 ARC  下不能直接调用 dealloc。ARC 会自动在 .cxx_destruct 方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的 dealloc 方法。ARC 环境下,dealloc 方法可以像这样写:

1 - (void)dealloc {
2 
3   CFRelease(_coreFoundationObject);
4 
5   free(_heapAllocatedMemoryBlob);
6 
7 }

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

覆写内存管理方法

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

END

 

posted @ 2017-08-07 01:04  鳄鱼不怕牙医不怕  阅读(335)  评论(0编辑  收藏  举报