iOS内存管理之三:ARC(Automatic Reference Counting)
在上一篇文章中,我们主要
介绍了基于MRC环境下的内存管理。这篇文章主要介绍基于ARC环境下的内存管理。从WWDC2011到现在已经有近5年的时间,ACR机制的应用已经十分成熟,如今在Xcode中新建项目,都默认开启ARC。下面我会从ARC的原理到使用进行详细讲解。
一、什么是ARC
ARC——Automatic Reference Counting,自动引用计数。它不是运行时特性,不是垃圾回收器(GC),而是一种编译时特性。
Automatic Reference Counting (ARC) is a compiler-level feature that simplifies the process of managing object lifetimes (memory management) in Cocoa applications.
与MRC模式相比,在ARC模式下会减少相应的工作量。为什么这样说呢?因为在ARC模式下编写代码,不需要写retain
、release
、autorelease
这三个关键字来对实例对象进行手动管理内存,这会减少很多代码。当开启ARC时,编译器在编译代码时会自动在代码合适的地方插入retain
、release
和autorelease
。也就是说,原来在MRC模式下需要写的类似于[obj release]
这样的代码,在ARC模式下编译器会自动帮我们完成,不需要我们去写,这就是所谓的自动引用计数。这样会相应地提高开发效率。
二、ARC工作原理
ARC模式的基本原理与MRC相同,都是引用计数原理,只是书写方式不同。在MRC模式下,如果想要保持一个对象使其不被释放,需要使用retain
关键字。在ARC模式下要做的就是用一个指针指向这个对象,只要指针没有被置空,对象就会一直保持在堆上。当将指针指向新值时,原来的对象会被release一次。
ARC可以为开发者节省很多代码,使用ARC以后再也不需要关心什么时候retain,什么时候release,但是这并不意味你可以不思考内存管理,我们需要经常性地问自己这个问题:谁持有这个对象?
1.“持有”概念
在ARC中,我们说对象A“持有”对象B,就是说对象A“强引用”对象B。写法如下:
1 | NSObject * obj = [[NSObject alloc] init]; |
引用又分为强引用和弱引用。被 strong
关键字修饰的对象A,如果指向对象obj,即obj被一个 strong
指针指向,obj被强引用,则obj不会销毁。如果对象没有任何 strong
指针指向,那么就讲销毁。被 weak
关键字修饰的对象B,如果指向对象obj,那么对象obj被一个 weak
指针指向,obj被弱引用,obj是否销毁与其无关。
一个 weak
指针P指向一个对象obj,并没有增加P的引用计数。另外,在ARC模式下,所有对象指针类型默认为 strong
类型。
2.理解strong和weak
strong
和 weak
类似于MRC模式下的 retain
和 assign
。请看下图:
在上图中,有两个 strong
类型指针A和B指向O,一个 weak
类型指针C指向O。每有一个 strong
类型指针指向O,在编译时,对象O会进行 [O retain]
一次,此时对象O的引用计数为2。weak
指针对其引用计数没有影响。当对象A或者对象B不再指向O时,对象O的引用计数减1,当没有对象持有时,进行释放。说到底,ARC模式的管理方式还是基于引用计数。
三、ARC修饰符
在ARC环境下,有4个与内存相关的变量所有权修饰符,他们分别是:
- __strong
- __weak
- __autoreleasing
- __unsafe_unretained
这里所说的变量所有权修饰符,与属性(property)中的属性修饰符不同,他们有如下对应关系:
assign
对应的所有权类型是__unsafe_unretained
copy
对应的所有权类型是__strong
retain
对应的所有权类型是__strong
strong
对应的所有权类型是__strong
unsafe_unretained
对应的所有权类型是__unsafe_unretained
weak
对应的所有权类型是__weak
关于属性修饰符,后面我会写一篇关于 property
的文章进行详细介绍,在此暂时不做介绍。接下来主要介绍一下4个变量所有权修饰符。
1.__strong
__strong
表示引用为强引用。对应定义 property 时用到的 strong
。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。
__strong
修饰的变量会自动初始化为 nil
。
2.__weak
__weak
表示弱引用,对应定义 property 时用到的 weak
。__weak
最常见的一个作用就是用来避免强引用循环。但是需要注意的是,__weak
修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained
修饰符来代替。关于 __weak
,有以下几点需要注意:
(1)弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。如下图:
对于对象N,开始有一个强引用指针A和一个弱引用指针B指向它,之后A指向M,没有强引用指针指向N,N被释放,此时弱引用指针B自动被置为 nil
,防止变为野指针。
(2)__weak
主要用来避免循环引用,主要有以下几个应用场景:
- 在使用
delegate
时,我们需要将delegate
的属性定义为weak
,以避免强引用循环。
1 | ClassOneVC: |
- 在 Block 中防止强引用循环,后面细讲。
- 用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIImageView myImgV。
对于在类中使用的UIKit控件,一般为
strong
类型,至于为什么在Interface Builder或者StoryBoard中创建的控件可以用weak
,有一种解释是在Interface Builder或者StoryBoard中进行了strong,具体什么原理还请大神解答。
3.__autoreleasing
用 __autoreleasing
修饰一个对象,表示这个对象被添加到 autorelease pool
中自动释放引用。这和MRC模式下的 autorelease
的用法相同。只不过在MRC模式下,不能够再显示使用 autorelease
方法了,但是 autorelease
的机制还是有效的,即通过使用 autorelease
修饰对象。
下面两行代码意义相同:
1 | NSString * str = [[[NSString alloc] initWithFormat:@"hello"] autorelease]; //MRC |
另外,定义property时不能使用这个修饰符,因为任何一个对象的property都不应该是 autorelese
类型。
在ARC模式下,使用(隐式使用)__autoreleasing
的几个场景:
- 方法返回值
- 访问
__weak
修饰的变量 - id类型指针
- 指针的指针(id *)
- 某些类方法隐式创建自己的
autorelease pool
id 类型类似于(NSObject ),所以(id )类似于(NSObject ** )。
(1)方法返回值
请看下面代码:
1 | - (NSObject *)myObject { |
在这个方法中,obj
的默认所有权修饰符为 __strong
。当return时,使 obj
超出其作用域,它强引用持有的对象本应该释放,但是由于该对象作为方法的返回值,所以一般情况下编译器会自动将 obj
注册到 Autorelease Pool中。这样就延长了对象的生命周期,使其出了作用域之后,还能够使用。当Autorelease Pool 被销毁的时候,对象的生命周期才会结束。
Autorelease Pool 是与线程一一映射的,这就是说一个 autoreleased 的对象的延迟释放是发生在它所在的 Autorelease Pool 对应的线程上的。。因此,在方法返回值的这个场景中,如果 Autorelease Pool 的 drain 方法没有在接收方和提供方交接的过程中触发,那么 autoreleased 对象是不会被释放的。所以不必担心 “Autorelease Pool 都销毁了,接收方还没接收到对象”这样的问题。
关于Autorelease Pool何时释放,生命周期的问题,实现原理等问题,可以参考这篇文章:黑幕背后的Autorelease。
#####(2)访问 __weak
修饰变量
当访问由 __weak
修饰的变量时,实际访问的是注册到 Autorelease Pool中的对象,例如下面两段代码意义相同:
1 | NSObject *obj0 = [NSObject new]; |
这样做是为了延长对象的生命周期。因为在 __weak
修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。
(3) id类型指针
一个被引用过几百遍的例子,如在使用NSError时:
1 | NSError *__autoreleasing error; |
在上面的代码中,如果error定义为 strong
类型,即使不用 __autoreleasing
修饰,编译器也会帮你自动添加,保证你传入的是一个 autoreleaing
类型的引用,如下(意义与上段代码相同):
1 | NSError *error; |
但是为了提高程序效率,我们在定义的error的时候,一般都声明为 autoreleasing
类型。
(4)指针的指针
在ARC环境下,所有种指针的指针类型(id *)的函数参数如果不加修饰符,编译器会默认将他们认定为 __autoreleasing
类型。例如下面两段代码等价:
1 | - (void)myFunc:(NSObject **)obj |
1 | - (void)myFunc:(NSObject * __autoreleasing *)obj |
(5)类方法隐式创建 Autorelease Pool
某些类的方法会隐式地使用自己的Autorelease Pool,例如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:
1 | - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error |
上面代码中,会隐式创建一个Autorelease Pool,等价于:
1 | - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error |
为了能够正常的使用*error,我们需要一个strong型的临时引用,在dict的枚举Block中是用这个临时引用,保证引用指向的对象不会在出了dict的枚举Block后被释放,正确的方式如下:
1 | - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error |
4.__unsafe_unretained
ARC是在iOS 5引入的,而这个修饰符主要是为了在ARC刚发布时兼容iOS 4以及版本更低的设备,因为这些版本的设备没有weak pointer system,简单的理解这个系统就是我们上面讲weak时提到的,能够在 weak
引用指向对象被释放后,把引用值自动设为 nil
的系统。这个修饰符在定义property时对应的是”unsafe_unretained”,实际可以将它理解为MRC时代的 assign
:纯粹只是将引用指向对象,没有任何额外的操作,在指向对象被释放时依然原原本本地指向原来被释放的对象(所在的内存区域)。所以非常不安全。
现在可以完全忽略掉这个修饰符了,因为iOS 4早已退出历史舞台,目前的APP基本都不会再去兼容iOS4。
四、ARC中的Block
一般情况下,block捕获的外部变量,可以在block内部使用,但是无法修改,例如下面代码:
1 | { |
注:static的变量和全局变量不需要加__block就可以在Block中修改
如果修改 str
,编译器会报错。如果想要修改 str
,需要用 __block
修饰符修饰要修改的变量,但也会引入新的问题,请看下面示例:
1 | { |
在上面这段代码中,myController
的 completionHandler
调用了 myController
的方法[dismissViewController…],这时 completionHandler
会对 myController
做 retain
操作。而我们知道,myController
对 completionHandler
也至少有一个retain(一般准确讲是copy),这时就出现了在内存管理中最糟糕的情况:循环引用!
简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。
针对以上问题,如果循环引用已经产生了,我们可以这样去解决:
1 | { |
为了避免循环引用,大家可能想到这样一个方法:
1 | { |
在上述代码中,我们让block捕获了一个弱引用,即 weakMyController
。但是问题又来了:block如果捕获一弱引用,在编译后会将其捕获在自己的函数栈中,当block函数执行完毕,就会释放这个弱引用。那么当myController指向的对象在completionHandler被调用前释放,那么completionHandler就不能正常的运作了。在一般的单线程环境中,这种问题出现的可能性不大,但是到了多线程环境,就很不好说了。
针对这个问题,有引入了下面的最佳解决方案:
1 | { |
block内部定义了一个强引用,这就保证捕获的弱引用 weakMyController
在block函数栈运行结束后不会释放。如果说block存在于堆上,那么 strongMyController
作为block的成员,也会存在于堆上,只有在blokc销毁时,它才会销毁。
关于理解被Block捕获的引用和在Block内定义的引用的区别,及block底层原理,请看唐巧这篇关于block的文章。
最后关于block再说一点。__block在MRC时代有两个作用:
- 说明变量可改
- 说明指针指向的对象不做这个隐式的retain操作,用于避免循环引用。
在ARC模式下,__block修饰符只说明变量可修改。
五、ARC与Toll-Free Bridging
Toll-Free Briding 保证了在程序中,可以方便和谐的使用 Core Foundation 类型的对象和Objective-C 类型的对象。
1.问题的引入
在 MRC 时代,由于 Objective-C 类型的对象和 Core Foundation 类型的对象都是相同的 release 和 retain 操作规则,所以 Toll-Free Bridging 的使用比较简单,但是自从切换到 ARC 后,Objective-C 类型的对象内存管理规则改变了,不能使用release和retain操作,而 Core Foundation 依然是之前的机制,也就是说,Core Foundation 不支持 ARC。
这时候我们就需要解决一个问题:在做 Core Foundation 与 Objective-C 类型转换的时候,我们不仅要做类型转换,还要将其内存管理规则进行转换。
于是苹果在引入 ARC 之后对 Toll-Free Bridging 的操作也加入了对应的方法与修饰符,用来指明用哪种规则管理内存,或者说是内存管理权的归属。这些方法和修饰符分别是:
- __bridge(修饰符)
- __bridge_retained(修饰符) or CFBridgingRetain(函数)
- __bridge_transfer(修饰符) or CFBridgingRelease(函数)
#####(1)__bridge
只是声明类型准换,不做内存管理规则转换。例如:
1 | { |
只是做了类型的转化,但管理规则未变,依然要用 Objective-C 类型的 ARC 来管理 s1,你不能用 CFRelease() 去释放 s1。
#####(2)__bridge_retained or CFBridgingRetain
表示将指针类型转变的同时,将内存管理的责任由原来的 Objective-C 交给Core Foundation 来处理,也就是,将 ARC 转变为 MRC。例如:
1 | { |
这时内存管理规则由ARC变为了MRC,我们需要手动的来管理s2的内存,而对于s1,我们即使将其置为nil,也不能释放内存。
上面代码也等价于:
1 | { |
(3)__bridge_transfer(修饰符) or CFBridgingRelease(函数)
这个修饰符和函数的功能和上面那个__bridge_retained相反,它表示将管理的责任由Core Foundation转交给Objective-C,即将管理方式由MRC转变为ARC。
1 | { |
这里我们将result的管理责任交给了ARC来处理,我们就不需要再显式调用CFRelease()了。
六、循环引用
在ARC模式下,不用我们去手动管理内存,这方便了很多,也减少了很多工作量。但是ARC模式也有它自己需要注意的问题,那就是循环引用。
1.什么是循环引用
如下图中,对象A和对象B,相互引用对方作为自己的成员变量,只有当对象销毁时,才会将成员变量的计数器减1。但是对象A的销毁依赖于对象B的销毁,对象B的销毁依赖于对象A的销毁。他们互相依赖,谁都不能销毁,这就造成了循环引用。这样即使没有其他强引用指针指向它们,它们也不会销毁。
简单代码示例:
1 | - (void)viewDidLoad { |
使用Instruments测试结果:
还有一种复杂的循环引用情景,那就是多个对象间依次持有,形成一个环状,这也会造成循环引用问题。例如下图中的情况:
在实际项目开发中,项目的环境比较大,所以一旦产生这种多个对象之间的循环引用,修改起来十分繁琐,所以在实际开发中,应当注意。
2.容易产生循环引用场景
iOS开发中,有三个场景容易造成循环引用:
- block 使用
- delegate 使用
- NSTimer 使用
具体如何产生于解除,请看这篇文章。
3.避免和解除循环引用
1.如果想要避免产生循环引用,最长见的就是使用弱引用 (weak reference
)。弱引用虽然持有对象,但是不增加引用计数,这样就避免了循环引用的产生。
2.如果循环引用已经产生,想要解除循环引用的话,需要开发者手动断开依赖对象。所以如果知道在什么时候断开循环引用回收内存,那就在相应的位置将对象手动置为 nil
。
有关ARC模式下内存管理的内容,就写到这里。还请大家勘误。下一篇将介绍几种简单的内存优化方案。
参考
1.Beginning ARC in iOS 5 Tutorial Part 1
4.iOS开发进阶