ARC是什么
ARC是ios5推出的新功能,全称叫ARC(Automatic Reference Counting)。简单地说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。
简单理解ARC,就是通过指定的语法,让编译器(LLVM3.0)在编译代码时,自动生成实例的引用计数管理部分代码。有一点,ARC并不是GC,它只是一种代码静态分析(static Analyzer)工具。
ARC的本质
ARC是编译器特性,而不是运行时特性,更不是垃圾回收器(CG)。
ARC的开启与关闭
打开ARC:-fobjc-arc
关闭ARC:-fno-objc-arc
ARC的修饰符
__strong:表示引用为强引用。对应的定义property时的”strong”。所有对象只有当没有任何一个强引用指向时,才会被释放。
注意:如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要将强引用置nil。
__weak:表示引用为弱引用。对应在定义property时的”weak”。弱引用不会影响对象的释放,即只要对象没有任何一个强引用指向,即使有100个弱引用对象指向也没用,该对象会被释放。不过好在,对象在被释放的同时,指向它的弱引用会自动被置nil,这个技术叫zeroing weak pointer。这样有效得防止无效指针,野指针的产生。__weak一般用在delegate关系中防止循环引用或者用来修饰指向由Interface Builder编辑与生成的UI控件。
__autoreleasing:表示在autorelease pool中自动释放对象的引用,和MRC时代autorelease的用法相同。定义property时不能使用这个修饰符,任何一个对象的property都不应该是autorelease型的。
以下两行代码的意义是相同的。
为了提高效率,避免这种情况,我们一般在定义error的时候将其声明为__autorelease类型的
*error指向的对象在创建出来后,被放入到了autorelease pool中,等待使用结束后的自动释放,函数外error的使用者并不需要关心*error指向对象的释放。
另外一点,在ARC中,所有这种指针的指针(NSError**)的函数参数如果不加修饰符,编译器会默认将他们认定为__autoreleasing类型。
比如下面的两端代码是等同的
除非你显式得给value声明了__strong,否则value默认就是__autoreleasing的。
最后一点,某些类的方法会隐式地使用自己的autorelease pool,在这种时候使用__autoreleasing类型要特别小心。
比如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:
会隐式地创建一个autorelease pool,上面代码实际类似于:
为了能够正常的使用*error,我们需要一个strong型的临时引用,在dict的枚举Block中是用这个临时引用,保证引用指向的对象不会在出了dict的枚举Block后被释放,正确的方式如下:
__unsafe_unretained:ARC是在iOS 5引入的,而这个修饰符主要是为了在ARC刚发布时兼容iOS 4以及版本更低的设备,因为这些版本的设备没有weak pointer system,简单的理解这个系统就是我们上面讲weak时提到的,能够在weak引用指向对象被释放后,把引用值自动设为nil的系统。这个修饰符在定义property时对应的是”unsafe_unretained”,实际可以将它理解为MRC时代的assign:纯粹只是将引用指向对象,没有任何额外的操作,在指向对象被释放时依然原原本本地指向原来被释放的对象(所在的内存区域)。所以非常不安全。
使用修饰符的正确姿势
要定义一个weak型的NSString引用,它的写法应该是:
栈中指针默认值为nil
无论是被strong,weak还是autoreleasing修饰,声明在栈中的指针默认值都会是nil。所有这类型的指针不用再初始化的时候置nil了。虽然好习惯是最重要的,但是这个特性更加降低了“野指针”出现的可能性。
在ARC中,以下代码会输出null而不是crash:)
ARC与Block
在MRC时代,Block会隐式地对进入其作用域内的对象(或者说被Block捕获的指针指向的对象)加retain,来确保Block使用到该对象时,能够正确的访问。
这件事情在下面代码展示的情况中要更加额外小心。
在这段代码中,myController的completionHandler调用了myController的方法[dismissViewController…],这时completionHandler会对myController做retain操作。而我们知道,myController对completionHandler也至少有一个retain(一般准确讲是copy),这时就出现了在内存管理中最糟糕的情况:循环引用!简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。我们在delegate关系中,对delegate指针用weak就是为了避免这种问题。
不过好在,编译器会及时地给我们一个警告,提醒我们可能会发生这类型的问题:
对这种情况,我们一般用如下方法解决:给要进入Block的指针加一个__block修饰符。
这个__block在MRC时代有两个作用:
1.说明变量可改
2.说明指针指向的对象不做这个隐式的retain操作
一个变量如果不加__block,是不能在Block里面修改的,不过这里有一个例外:static的变量和全局变量不需要加__block就可以在Block中修改。
使用这种方法,我们对代码做出修改,解决了循环引用的问题:
在ARC引入后,没有了retain和release等操作,情况也发生了改变:在任何情况下,__block修饰符的作用只有上面的第一条:说明变量可改。即使加上了__block修饰符,一个被block捕获的强引用也依然是一个强引用。这样在ARC下,如果我们还按照MRC下的写法,completionHandler对myController有一个强引用,而myController对completionHandler有一个强引用,这依然是循环引用,没有解决问题:(
于是我们还需要对原代码做修改。简单的情况我们可以这样写:
在completionHandler之后将myController指针置nil,保证了completionHandler对myController强引用的解除,不过也同时解除了myController对myController对象的强引用。这种方法过于简单粗暴了,在大多数情况下,我们有更好的方法。
这个更好的方法就是使用weak。(或者为了考虑iOS4的兼容性用unsafe_unretained,具体用法和weak相同,考虑到现在iOS4设备可能已经绝迹了,这里就不讲这个方法了)(关于这个方法的本质我们后面会谈到)
为了保证completionHandler这个Block对myController没有强引用,我们可以定义一个临时的弱引用weakMyViewController来指向原myController的对象,并把这个弱引用传入到Block内,这样就保证了Block对myController持有的是一个弱引用,而不是一个强引用。如此,我们继续修改代码:
这样循环引用的问题就解决了,但是却不幸地引入了一个新的问题:由于传入completionHandler的是一个弱引用,那么当myController指向的对象在completionHandler被调用前释放,那么completionHandler就不能正常的运作了。在一般的单线程环境中,这种问题出现的可能性不大,但是到了多线程环境,就很不好说了,所以我们需要继续完善这个方法。
为了保证在Block内能够访问到正确的myController,我们在block内新定义一个强引用strongMyController来指向weakMyController指向的对象,这样多了一个强引用,就能保证这个myController对象不会在completionHandler被调用前释放掉了。于是,我们对代码再次做出修改:
到此,一个完善的解决方案就完成了:)
理解这个问题的关键在于理解被Block捕获的引用和在Block内定义的引用的区别。
为了更清楚地说明问题,这里用一个简单的程序举例。比如我们有如下程序:
程序中,同为int型的指针,a是被Block捕获的变量,而c是在Block内定义的变量。我们用clang -rewrite-objc处理后,可以看到如下代码:
Block的结构:
实际执行的函数:
我们可以清楚得看到,a和c存在的位置完全不同,如果Block存在于堆上(在ARC下Block默认在堆上),那么a作为Block结构体的一个成员,也自然会存在于堆上,而c无论如何,永远位于Block内实际执行代码的函数栈内。这也导致了两个变量生命周期的完全不同:c在Block的函数运行完毕,即会被释放,而a呢,只有在Block被从堆上释放的时候才会释放。
回到我们的MyViewController的例子中,同上理,如果我们直接让Block捕获我们的myController引用,那么这个引用会被复制后(引用类型也会被复制)作为Block的成员变量存在于其所在的堆空间中,也就是为Block增加了一个指向myController对象的强引用,这就是造成循环引用的本质原因。对于MyViewController的例子,Block的结构体可以理解是这个样子:(准确的结构体肯定和以下这个有区别,但也肯定是如下这种形式:)
而反观我们给Block传入一个弱引用weakMyController,这时我们Block的结构:
再看在Block内声明的强引用strongMyController,它虽然是强引用,但存在于函数栈中,在函数执行期间,它一直存在,所以myController对象也一直存在,但是当函数执行完毕,strongMyController即被销毁,于是它对myController对象的强引用也被解除,这时Block对myController对象就不存在强引用关系了!加入了strongMyController的函数大体会是这个样子:
综上所述,在ARC下(在MRC下会略有不同),Block捕获的引用和Block内声明的引用无论是存在空间与生命周期都是截然不同的,也正是这种不同,造成了我们对他们使用方式的区别。
以上就解释了之前提到的所有问题,希望大家能看明白:
好的,最后再提一点,在ARC中,对Block捕获对象的内存管理已经简化了很多,由于没有了retain和release等操作,实际只需要考虑循环引用的问题就行了。比如下面这种,是没有内存泄露的问题的:
我们上面提到的解决方案,只是针对Block产生循环引用的问题,而不是说所有的Block捕获引用都要这么处理,一定要注意!
ARC与Toll-Free Bridging
在MRC时代,由于Objective-C类型的对象和Core Foundation类型的对象都是相同的release和retain操作规则,所以Toll-Free Bridging的使用比较简单,但是自从ARC加入后,Objective-C类型的对象内存管理规则改变了,而Core Foundation依然是之前的机制,换句话说,Core Foundation不支持ARC。
这个时候就必须要要考虑一个问题了,在做Core Foundation与Objective-C类型转换的时候,用哪一种规则来管理对象的内存。显然,对于同一个对象,我们不能够同时用两种规则来管理,所以这里就必须要确定一件事情:哪些对象用Objective-C(也就是ARC)的规则,哪些对象用Core Foundation的规则(也就是MRC)的规则。或者说要确定对象类型转换了之后,内存管理的ownership的改变。
__bridge(修饰符)
只是声明类型转变,但是不做内存管理规则的转变。
只是做了NSString到CFStringRef的转化,但管理规则未变,依然要用Objective-C类型的ARC来管理s1,你不能用CFRelease()去释放s1。
__bridge_retained(修饰符) or CFBridgingRetain(函数)
表示将指针类型转变的同时,将内存管理的责任由原来的Objective-C交给Core Foundation来处理,也就是,将ARC转变为MRC。
比如,还是上面那个例子
我们在第二行做了转化,这时内存管理规则由ARC变为了MRC,我们需要手动的来管理s2的内存,而对于s1,我们即使将其置为nil,也不能释放内存。
等同的,我们的程序也可以写成:
__bridge_transfer(修饰符) or CFBridgingRelease(函数)
这个修饰符和函数的功能和上面那个__bridge_retained相反,它表示将管理的责任由Core Foundation转交给Objective-C,即将管理方式由MRC转变为ARC。
比如:
这里我们将result的管理责任交给了ARC来处理,我们就不需要再显式地将CFRelease()了。
对了,这里你可能会注意到一个细节,和ARC中那个4个主要的修饰符(__strong,__weak,…)不同,这里修饰符的位置是放在类型前面的,虽然官方文档中没有说明,但看官方的头文件可以知道。小伙伴们,记得别把位置写错哦:)
下图为今年部分iOS开发的视频教程,因为不定时更新中故不做多的截图,如果有iOS开发上的问题不懂或者需要视频教程可以看我的个人简介。
不定时更新中。
网友评论