了解iOS 6开发第三篇:内存管理 - 手动引用计数与自动引用计数

本文不会举例介绍MRC,如果不了解MRC,最好先简单了解一下,再回来读这篇文章。

从iOS 5开始,iOS开发就可以使用自动引用计数(Automatic Reference Counting)了。一直想写一篇详细的笔记,来整理这方面的信息,但一直拖到现在。这篇文章是
WWDC 2012 Session 406 - Adopting Automatic Reference Counting的笔记,如果对LLVM感兴趣,可以去llvm.org深入了解一下。

关于自动引用计数(Automatic Reference Counting, aka ARC)

很多工程师都是最近一、两年才开始接触iOS开发的。但大多数工程师都接触过Java/C#这样的静态编译语言,或者python/ruby这样的动态脚本语言。这些“现代”语言的都提供Garbage Collection的机制,由GC来负责内存的回收。我先强调一点:ARC是编译器提供的机制,而不是GC这种运行时提供的机制。要了解ARC,必须明白什么是引用计数,必须从MRC开始。此外,你会遇到很多旧的开源代码和静态库(比如广告平台)。这些很可能不支持ARC,所以还是要理解MRC。

关于手动引用计数(Manully Reference Counting, aka MRC)

Objective-C的MRC,简单来说就是一句话:谁创建,谁释放。复杂一点的解释如下:

  • 所有由alloc、copy(mutablecopy)、retain、new创建的object,必须手动release(或autorelease)
  • 反之,则不需要也不可以
  • autorelease不是自动释放,是AutoreleasePool的实例,在runloop中被”稍后“释放。

MRC有什么问题

常犯的错误:

  • Crash! Reject! 内存管理是导致app crash的最主要原因。也是App Store审核时拒审的最多的原因;
  • 规则简单,但细节太多,依赖于工程师的清醒头脑和良好习惯,依赖于命名规范
  • Dangling Pointer,delegate需要手动设置为nil
  • 触发NSError、或者有复杂的条件分支逻辑时,忘记释放内存

需要使用一堆工具,帮助你检测代码:

  • Instruments: Allocations, Leaks, Zombies
  • Static Analyzer
  • Heap
  • ObjectAlloc
  • vmmap
  • Debugger

工程师需要做的事情:

  • 理解MRC,写正确的代码
  • 正确的命名规范
  • 使用五花八门的辅助工具,来保证代码没问题
  • 保持清醒的头脑,不犯低级错误

这不是工程师所擅长的事情,这个工作应该由编译器来做才对—LLVM 3.1

什么是ARC

  • Objective-C对象的自动管理机制
  • 由LLVM编译器来保证
  • 与MRC完全兼容
  • 引入新的运行时特性:弱指针、性能优化

ARC不是

  • 新的运行时模型
  • 自动malloc/free, CF等等(不负责纯C代码的内存管理)
  • 不是GC。不扫描堆、不暂停进程运行、没有不确定的内存释放

ARC如何工作

  • 编译器在编译期,帮你动态添加retain/release/autorelease
  • 返回同样结果的不同API,在内存方面,没有区别(把alloc/init模式与convenient method当成一样就可以了)

ARC有什么好处

  • 不用写retain/release/autorelease这样的代码,甚至连dealloc方法都不需要(前提是类不需要管理C指针)
  • 更好的性能,ARC智能地在”适当“的地方插入retain/release
  • Block just work
  • Less code, less bug
  • 少浪费脑细胞和脑时钟周期

如何打开ARC

  • Xcode的Project Setting中,设置Objective-C Automatic Reference Counting为YES
  • Xcode 4.2+,新的Project默认为ARC

为了实现ARC,LLVM编译器必须保证4条规则

  • 不能调用或实现跟引用计数相关的方法(retain/release/autorelease/retaincount等),否则产生编译错误
  • C的结构体里面,不能有Object指针;如果必须使用,那么可以用Objective-C的类代替
  • 编译器必须清楚void*是否被retained。引入新的API来转换Objective-C和Core Foundation类型的对象
  • 编译器必须清楚自动释放的对象,因此AutoreleasePool不能是对象,而只是语义上的符号。@autoreleasepool directive甚至可能在MRC中使用

如何理解ARC

  • 从对象的从属关系角度考虑:强引用(retain)保证对象的存在,或没有强引用,则对象被自动销毁
  • 想清楚程序的对象图
  • 不要再考虑retain, release, autorelease了

弱引用(必须在iOS 5中使用)

  • 安全的”弱指针“,引用的对象被销毁时,自动变成nil;避免了Dangling Pointer
  • 在property中使用weak关键字声明,ivar中使用_weak声明

循环引用

  • ARC依然存在循环引用的可能
  • 手动设置某一个引用为nil,破坏循环引用
  • 使用弱引用来避免

性能

  • 与MRC没有实质的性能区别,甚至更好(有时候好得多),内存峰值比MRC低
  • 没有GC的开销;没有延迟的销毁,没有进程暂停,没有不能确定的释放

ARC的优点

  • 更容易理解
  • 写更少的代码,更少的bug
  • 更容易维护,甚至更好的性能
  • 安全、稳定

iOS的什么版本才能支持ARC

  • iOS 6发布后,以iOS 5.0作为Deployment Target完全没问题。所以都可以使用ARC。如果你必须支持5.0以下的设备,那么看后两条。
  • ARC由LLVM 3.1保证,LLVM 3.1在安装Xcode 4.2自动安装;也就是说只要Xcode 4.2以上的版本,可以设置的Deployment Target的最低版本,都支持ARC。笔者在4.0+的app中使用ARC。
  • 弱引用(Weak Reference)必须在iOS 5以上的设备才支持

总结

笔者其实觉得MRC很容易掌握,profile的工具也容易掌握。但ARC实实在在的从安全、可维护性、代码量、性能上有全面的提升。所以除非维护旧的代码,笔者都建议使用ARC。不用担心app依赖第三方的MRC代码,可以通过编译器设置文件的编译属性来混合使用ARC和MRC。

如果想更多地了解LLVM是如何智能地保证ARC,请阅读文章头部引用的两篇文章,里面有详细的解释和代码例子。

附小广告一则:唱吧iOS团队诚招iOS工程师,推荐成功即奖励6000元现金或iPhone 6一部,详见这篇blog