历史版本

ARC(Automatic Reference Counting,自动引用计数)极大地减少了Cocoa开发中的常见编程错误:retainrelease不匹配。ARC并不会消除对retainrelease的调用,而是把这项原本大都属于开发者的工作移交给了编译器。这样做的好处是显而易见的,但是必须知道retainrelease是仍然在使用的。ARC并不等同垃圾回收。思考下面这段代码,它对一个实例变量赋值:

1
2
3
@property (nonatomic, readwrite, strong) NSString *title;
...
_title = [NSString stringWithFormat:@"Title"];

在非ARC环境中,_title没有被保留过,那么赋给它的NSString就是autorelease的,会在事件循环结束时被释放。之后如果再次访问_title,程序就会崩溃。这类错误极其常见,调试起来会异常困难。另外,如果_title之前已经有一个值,那旧的值就会因为没有被释放掉而引起内存泄漏。

使用ARC,编译器会在合适的位置自动插入一些代码,下面的代码与前面的代码是等同的:

1
2
3
4
id oldTitle = _title;
_title = [NSString stringWithFormat:@"Title"];
[_title retain];
[oldTitle release];

retainrelease仍然会被调用,所以有一些开销,在release的时候可能还会调用dealloc方法。这段代码与程序员手动调用retainrelease的代码在运行结果上是完全一致的。垃圾回收机制是在运行时起作用的,会影响运行效率,而ARC是在编译时插入内存管理代码,不影响运行时效率,因此内存回收比垃圾回收时的效率要高,能够提升系统性能。正如其他一些编译器优化方式一样,这种编译器可以自由地以多种方式优化内存管理,而让程序员手动去做这些工作是不现实的。在多数情况下,使用ARC生成内存管理代码的程序比程序员手工添加内存管理代码的对等程序运行更快!

ARC不是垃圾回收,尤其是它不能像Snow Leopard中的垃圾回收机制那样处理循环引用(保留)。图3-1中的对象A与对象B之间存在循环保留。

图3-1 循环保留

在Snow Leopard的垃圾回收机制下,如果从“外部对象”到“对象A”的引用链接中断了,对象A和对象B都会被销毁,因为它们从程序中孤立出去了。而在ARC中,对象A和对象B都不会被销毁,因为它们的引用计数都大于0。因此,在iOS开发中,必须要做好对强引用(strong reference)的跟踪管理以免出现循环引用。

属性关系有两种主要类型:strongweak,相当于非ARC环境里的retainassign。只要存在一个强引用,对象就会一直存在,不会被销毁。强引用类似于C++中的shared_ptr,只不过管理引用计数的代码是在编译时生成的,而shared_ptr是在运行时通过操作符重载确定的。

Objective-C中一直存在循环引用的问题,但在实际应用中很少出现循环引用。对于过去那些使用assign属性的地方,在ARC环境中要使用weak代替。大部分引用循环是由委托(delegate)引起的,所以应该总是把delegate属性声明为weak。当引用的对象被销毁之后,weak引用会被自动置为nil,与assign相比这是一个巨大的进步,因为assign可以指向被释放掉的内存,导致程序崩溃。


在ARC出现以前,合成属性(synthesized property)的默认存储类型是assign;而在ARC中,默认的存储类型是strong。这在进行代码转换时可能会造成一些困惑。建议为所有属性显式指明存储类型。 循环保留产生的另一个主要原因是块,其介绍详见第23章。


将代码转换到ARC的时候,要注意以下两点。

  • 不要再使用retainreleaseautorelease,可以直接将这些代码删除。ARC在编译时会自动在合适的位置插入内存管理代码。
  • 如果dealloc方法只是用于释放实例变量,那么dealloc方法也没有存在的必要了,可以直接删掉。在任何情况下都不需要再使用release,ARC会自动处理好这些事情。如果仍然需要使用dealloc来做一些其他的事情(比如移除KVO观察),记得不要再调用[super dealloc],否则编译器会给出错误信息。

正如前面所说的,ARC并不是垃圾回收,它是编译器的一种功能,可以在代码中合适的位置自动插入调用retainrelease的语句。这也意味着,如果现存的使用手动内存管理方式的代码很好地遵守了命名约定,那么这些代码跟ARC就是完全互通的。举例来说,如果调用了一个名为copySomething的方法,ARC会认为这个方法会对返回对象的引用计数加1,如果有必要,ARC会在合适的位置插入一个release。引用计数的加1操作既可以在ARC代码中进行,也可以在某段手动进行内存管理的代码中进行,这对于ARC来说是无所谓的。

但是,如果没有遵守Cocoa命名约定,这种情况下就会出错。比如有一个名为copyRight的方法,它以autorelease的形式返回一个版权信息的字符串,这种情况下ARC的行为取决于调用代码和被调用代码是否都是使用ARC进行编译的。

方法名是以copy开头的,因此ARC认为copyRight方法会对返回对象的引用计数加1。如果copyRight方法的代码与调用copyRight方法的代码都是使用ARC编译的,ARC就会在copyRight方法代码中添加一个retain语句,同时在调用copyRight方法的代码中添加一个release语句,这种情况下程序依然会正常工作。虽然对执行效率有一点点影响,但是程序不会崩溃,也不会造成内存泄漏。

然而,如果调用copyRight方法的代码是使用ARC编译的(ARC就会在代码后边添加一个release语句),而copyRight方法代码没有使用ARC编译的话(方法体中就没有retain语句),程序就会崩溃。反过来,如果copyRight方法代码使用ARC编译(ARC就会在方法代码中添加一个retain语句),而调用代码没有使用ARC编译的话(调用代码中就不会有release语句),代码就会造成内存泄漏。

最好的解决办法就是遵守Cocoa的命名约定。在这个例子中,如果将方法名改为copyright,就可以完全避免这种问题。ARC根据驼峰式大小写方式对方法的名称进行单词分隔,然后使用相应的内存管理规则。

如果对一个错误命名的方法进行重命名是不现实的,那么还可以在方法声明中使用NS_RETURNS_RETAINED或者NS_RETURNS_NOT_RETAINED修饰符来告诉编译器应该使用哪种内存管理规则。这些修饰符是在NSObjCRuntime.h中定义的。

为了让编译器能够恰当地添加retainrelease语句,编写ARC代码时需要注意以下四点。

  • 不要调用retainreleaseautorelease。直接将这些代码删除,这是最简单的规则。另外,也不可以覆盖这几个方法(永远不要试图去覆盖这几个方法)。如果你想实现Singleton模式,请阅读第4章,了解如何在不覆盖这些方法的情况下实现Singleton模式。

  • C语言结构体中不要有对象指针。这种情况很少出现。但是如果C语言结构体中确实有一个对象,要么把它存储到另一个对象中,要么就把它转换成void*类型(下一条规则会介绍如何转换到void*)。可以在任何时候通过调用free方法销毁一个C语言结构体,这会影响到对结构体中对象的自动跟踪。

  • idvoid*类型只能通过桥接转换进行转换。在使用了Core Foundation的代码中会有很多这种情况。第27章会详细介绍桥接转换及如何结合ARC来使用。

  • 不要使用NSAutoreleasePool。不要手动创建自己的自动释放池,直接把需要自己的池的代码放入@autoreleasepool{}代码块中即可。如果你有一些特殊的代码用于控制自动释放池的释放,那些代码实际上很可能是不必要的。@autoreleasepoolNSAutoreleasePool快了大约20倍。

对于大部分代码来说,只要遵守这些规则就不会有任何问题。在Xcode中有一个小工具可以替你完成大部分代码转换的工作,它位于Edit → Refactor → Convert to Objective-C ARC。

ARC应该是Objective-C中继自动释放池之后最重要的改进了,应该尽可能地使用。如果不能将代码全部转换到ARC,就尽可能地转换。使用ARC编译的程序速度更快,bug更少,也比手动内存管理代码更容易编写。现在就开始使用ARC吧!

【iOS】自动引用计数 (循环引用)的更多相关文章

  1. CCCallFuncN误用导致引用计数循环引用

    昨天测试“角色被遮挡部分透明显示”功能时,发现角色死亡后,其轮廓精灵不会消失.调试发现,角色在死亡时,其引用计数retain_count居然是9.这是由引用计数混乱引起的内存泄露. 加了很多日志跟踪r ...

  2. iOS 容易引“起循环引用”的三种场景

    笔者在阅读中总结了一下,在iOS平台容易引起循环引用的四个场景: 一.parent-child相互持有.委托模式 [案例]:   @interface FTAppCenterMainViewContr ...

  3. iOS 里面 NSTimer 防止 循环引用

    使用NSTimer的类 #import "TBTimerTestObject.h" #import "TBWeakTimerTarget.h" @interfa ...

  4. 从urllib2的内存泄露看python的GC python引用计数 对象的引用数 循环引用

    这里会发现上述代码是存在内存泄露,造成的原因就是lz与ow这两个变量存在循环引用,Python 不知道按照什么样的安全次序来调用对象的 __del__() 函数,导致对象始终存活在 gc.garbag ...

  5. nodejs中相互引用(循环引用)的模块分析

    话不多少,直接上源码吧: modA.js: module.exports.test = 'A'; const modB = require('./05_modB'); console.log( 'mo ...

  6. ios block 导致的循环引用

    [[NSNotificationCenter defaultCenter] addObserverForName:@"UIWindowDidRotateNotification" ...

  7. 【转】iOS学习之容易造成循环引用的三种场景

    ARC已经出来很久了,自动释放内存的确很方便,但是并非绝对安全绝对不会产生内存泄露.导致iOS对象无法按预期释放的一个无形杀手是——循环引用.循环引用可以简单理解为A引用了B,而B又引用了A,双方都同 ...

  8. 【原】iOS容易造成循环引用的三种场景,就在你我身边!

    ARC已经出来很久了,自动释放内存的确很方便,但是并非绝对安全绝对不会产生内存泄露.导致iOS对象无法按预期释放的一个无形杀手是——循环引用.循环引用可以简单理解为A引用了B,而B又引用了A,双方都同 ...

  9. Swift2.1 语法指南——自动引用计数

    原档: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programm ...

随机推荐

  1. VS2008+Windows DDK 7的环境配置

    Mark offers some third party utilities. That's good, but I will show a more handy way (IMHO): how to ...

  2. php之快速入门学习-5(常量)

    PHP 5 常量 常量值被定义后,在脚本的其他任何地方都不能被改变. PHP 常量 常量是一个简单值的标识符.该值在脚本中不能改变. 一个常量由英文字母.下划线.和数字组成,但数字不能作为首字母出现. ...

  3. 该内存不能为written

    #include <stdio.h>int main(){ char *p = "abc"; printf("%s\n", p); *p = 'd' ...

  4. selenium 加载jquery

    packagecom.example.tests; import staticorg.junit.Assert.*; importjava.util.*; importorg.junit.*; imp ...

  5. set names utf8 【mysql导入中文乱码问题解决】

    由于工作的关系需要经常导入一些sql脚本,但是经常会遇到导入中文出现乱码的问题,搞得自己每次导入sql脚本时都很害怕,自己也上网找了一些资料,尝 试了许多办法,有些方法可以正常导入中文,有些办法却不行 ...

  6. scala class和object,trait的区别

    Scala类 1 2 3 4 5 6 7 8 9 10 11 12 13 class Counter {   private var value = 0 // 必须初始化字段   def increm ...

  7. SlackWare安装

     Keep It Simple Stupid 01.下载 slackware: http://www.slackware.com/ 中科大:    http://mirrors.ustc.edu.cn ...

  8. 【laravel5.4】git上clone项目到本地,配置和运行 项目报错:../vendor/aotuload.php不存在

    1.一般我们直接使用git clone 将git的项目克隆下来,在本地git库和云上git库建立关联关系 2.vendor[扩展]文件夹是不会上传的,那么下载下来直接运行项目,会报错: D:phpSt ...

  9. Emacs 的版本控制功能

    All operations: C-x v + vc-update C-x v = vc-diff C-x v D vc-root-diff C-x v I vc-log-incoming C-x v ...

  10. 浅析iOS tableview的selectRowAtIndexPath选中无效(默认选中cell无效)

    可能很多人都遇到过这种情况: tableview列表,有时加载完,需要默认选中某一行,给予选中效果:或者需要执行某行的点击事件. 我们举例: 比如我想默认选中第一行 可能我们第一个想法就是这样: [m ...