iPhone开发深入浅出 — ARC


一、ARC是什么

         ARC是iOS

该机能在 iOS

 

变化点

通过一小段代码,我们看看使用ARC前后的变化点。

1. @interface NonARCObject : NSObject {
2.     NSString *name;
3. }
4. -(id)initWithName:(NSString *)name;
5. @end
6. 
7. @implementation NonARCObject
8. -(id)initWithName:(NSString *)newName {
9.     self = [super init];
10.              if (self) {
11.                  name = [newName retain];
12.              }
13.              return self;
14.          }
15.          
16.          -(void)dealloc {
17.              [name release];
18.              [Super dealloc];
19.          }
20.          @end
1. @interface ARCObject : NSObject {
2.     NSString *name;
3. }
4. -(id)initWithName:(NSString *)name;
5. @end
6. 
7. @implementation ARCObject
8. -(id)initWithName:(NSString *)newName {
9.     self = [super init];
10.              if (self) {
11.                  name = newName;
12.              }
13.              return self;
14.          }
15.          @end

我们之前使用Objective-C中内存管理规则时,往往采用下面的准则

 

•  生成对象时,使用autorelease

•  对象代入时,先autorelease后再retain

•  对象在函数中返回时,使用:

return [[object retain]autorelease];

而使用ARC后,我们可以不需要这样做了,甚至连最基础的release都不需要了。

使用ARC的好处

使用ARC有什么好处呢?

•  看到上面的例子,大家就知道了,以后写Objective-C的代码变得简单多了,因为我们不需要担心烦人的内存管理,担心内存泄露了

•  代码的总量变少了,看上去清爽了不少,也节省了劳动力

•  代码高速化,由于使用编译器管理引用计数,减少了低效代码的可能性

 

不好的地方

•  记住一堆新的ARC规则、关键字及特性等需要一定的学习周期

•  一些旧的代码,第三方代码使用的时候比较麻烦;修改代码需要工数,要么修改编译开关

 

关于第二点,由于 XCode4.2 中缺省ARC就是

ARC 是什么架构_ARC 是什么架构

这个时候,可以将项目编译设置中的“Objectice-C Auto Reference Counteting”设为NO。如下所示。

ARC 是什么架构_ARC_02

如果只想对某个.m文件不适应ARC,可以只针对该类文件加上

ARC 是什么架构_iOS_03

ARC基本规则

•  retain, release, autorelease, dealloc由编译器自动插入,不能在代码中调用.

•  dealloc虽然可以被重载,但是不能调用[super dealloc].

 

由于ARC并不是GC,并需要一些规则让编译器支持代码插入,所以必须清楚清楚了这些规则后,才能写出健壮的代码。

Objective-C对象

ObjectiveC中的对象,有强参照(Strong reference)和弱参照(Weak reference)之分,当需要保持其他对象的时候,需要retain以确保对象引用计数加1。对象的持有者(owner)只要存在,那么该对象的强参照就一直存在。

对象处理的基本规则是

 

•  只要对象的持有者存在(对象被强参照),那么就可以使用该对象

•  对象失去了持有者后,即被破弃强参照 (Strong reference)

ARC 是什么架构_ARC 是什么架构_04

(s1)firstName作为”natsu”字符串对象的最初持有者,是该NSString类型对象的Strong reference。

 

(s2)这里将firstName代入到aName中,即aName也成为了@”natsu”字符串对象的持有者,对于该对象,aName也是Strong reference。

 

(s3)这里,改变firstName的内容。生成新的字符串对象”maki”。这时候firstName成为”maki”的持有者,而@”natsu”的持有者只有aName。每个字符串对象都有各自的持有者,所以它们都在内存中都存在。

 

(s4)追加新的变量otherName, 它将成为@”maki”对象的另一个持有者。即NSString类型对象的Strong reference。

 

(s5)将otherName代入到aName,这时,aName将成为@”maki”字符串对象的持有者。而对象@”natsu”已经没有持有者了,该对象将被破弃。

 

弱参照 (Weak reference)

接下来我们来看看弱参照 (Weak reference) 的使用方式。

ARC 是什么架构_iOS_05

(w1)与强参照方式同样,firstName作为字符串对象@”natsu”的持有者存在。即是该NSString类型对象的Strong reference。

 

(w2)使用关键字__weak,声明弱参照weakName变量,将firstName代入。这时weakName虽然参照@”natsu”,但仍是Weakreference。即weakName虽然能看到@”natsu”,但不是其持有者。

 

(w3)firstName指向了新的对象@”maki”,成为其持有者,而对象@”natsu”因为没有了持有者,即被破弃。同时weakName变量将被自动代入nil。

 

引用关键字

ARC中关于对象的引用参照,主要有下面几关键字。使用strong, weak, autoreleasing限定的变量会被隐式初始化为nil。

__strong

变量声明缺省都带有__strong关键字,如果变量什么关键字都不写,那么缺省就是强参照。

 

__weak

上面已经看到了,这是弱参照的关键字。该概念是新特性,从 iOS

1. NSString __weak *string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];
2. NSLog(@"string: %@", string); //此时 string为空

 

如果编译设定OS版本

弱参照还有一个特征,即当参数对象失去所有者之后,变量会被自动赋值上nil

__unsafe_unretained

该关键字与__weak一样,也是弱参照,与__weak的区别只是是否执行nil赋值(Zeroing)。但是这样,需要注意变量所指的对象已经被破弃了,地址还还存在,但内存中对象已经没有了。如果还是访问该对象,将引起「BAD_ACCESS」错误。

__autoreleasing

该关键字使对像延迟释放。比如你想传一个未初始化的对象引用到一个方法当中,在此方法中实例化此对象,那么这种情况可以使用__autoreleasing。他被经常用于函数有值参数返回时的处理,比如下面的例子。

1. - (void) generateErrorInVariable:(__autoreleasing NSError **)paramError {
2.     ....
3.     *paramError = [[NSError alloc] initWithDomain:@"MyApp" code:1 userInfo:errorDictionary];
4. }
5. 
6. ....
7. {
8.     NSError *error = nil;
9.     [self generateErrorInVariable:&error];
10.              NSLog(@"Error = %@", error);
11.          }

又如函数的返回值是在函数中申请的,那么希望释放是在调用端时,往往有下面的代码。

1. -(NSString *)stringTest
2. {
3.     NSString *retStr = [NSString stringWithString:@"test"];
4. 
5.     return [[retStr retain] autorelease];
6. }
7. 
8. // 使用ARC
9. 
10.          -(NSString *)stringTest
11.          {
12.              __autoreleasing NSString *retStr = [NSString alloc] initWithString:@"test"];
13.          
14.              return retStr;
15.          }

即当方法的返回参数是id*,且希望方法返回时对象被autoreleased,那么使用该关键字。

总结

今天,我们看到了基本的ARC使用规则

 

•  代码中不能使用retain, release, retain, autorelease

•  不重载dealloc(如果是释放对象内存以外的处理,是可以重载该函数的,但是不能调用[super dealloc])

•  不能使用NSAllocateObject, NSDeallocateObject

•  不能在C结构体中使用对象指针

•  id与void *间的如果cast时需要用特定的方法(__bridge关键字)

•  不能使用NSAutoReleasePool、而需要@autoreleasepool块

•  不能使用“new”开始的属性名称

今后,我们将更加深入ARC,学习其更多的特性。

 

 

 

 

 

 

 

 

 

 

 

二、ARC对@property的使用规则影响

 

上一回我们学到了一些ARC的基本概念,这一次我们来看看ARC对@property的使用规则有何影响。

所有者属性

我们先来看看与所有权有关系的属性,关键字间的对应关系。

属性值

关键字

所有权

strong

__strong

weak

__weak

unsafe_unretained

__unsafe_unretained

copy

__strong

assign

__unsafe_unretained

retain

__strong


strong

该属性值对应 __strong 关键字,即该属性所声明的变量将成为对象的持有者。

 

weak

该属性对应 __weak 关键字,与 __weak 定义的变量一致,该属性所声明的变量将没有对象的所有权,并且当对象被破弃之后,对象将被自动赋值nil。并且,delegate

 

unsafe_unretained

等效于__unsafe_unretaind关键字声明的变量;像上面说明的,iOS

copy

与 strong 的区别是声明变量是拷贝对象的持有者。

 

assign

一般Scalar

 

retain

该属性与 strong 一致;只是可读性更强一些。

 

读写相关的属性 (readwrite, readonly)

读写相关的属性有 readwrite

比如下面的变量声明。

21.          @property (nonatomic, readonly) NSString *name;

一般声明为 readonly 的变量按理说应该不需要持有所有权了,但是在ARC有效的情况下,将出现下面的错误信息:“ARC forbids synthesizing a property of an Objective-C object with unspecified ownership or storage attribute”( ARC禁止合成一个objective - c对象所有权或存储属性不明的属性)

如果定义了ARC有效,那么必须要有所有者属性的定义;所以我们的代码改成这样,就OK了.

16.          @property (nonatomic, strong, readonly) NSString *name;

不过有一点,Scalar Varible的变量缺省都有 assign 的属性定义,所以不需要给他们单独的明示声明了。

 

 

 

ARC产生之前的 Objective-C 内存管理

 

前两节我们对 ARC(Automatic Reference Counting) 有了一个基本的理解,但是 ARC 是怎么产生的,为什么苹果要在其最新的 iOS/Mac OS X 上导入该框架? 如果不理解其背后的基本原理,只是死记硬背那些规则/方法,是毫无意义的。就像我们从小接受的填鸭式教育,基本上到后来都还给老师了。

本节,我们先来看看 ARC 产生之前的 Objective-C 内存管理世界,然后再来看看导入 ARC 后,新的 LLVM 编译器在背后为我们做了什么。

 

ARC 是什么架构_内存管理_06

Objective-C 内存管理

和许多面向对象语言一样,Objective-C 中内存管理的方式其实就是指 引用计数 (Reference Counting)的使用准则。如下图所示,对象生成的时候必定被某个持有者拿着,如果有多个持有者的话,其引用计数就会递增;相反失去一个持有者那么引用计数即会递减,直到失去所有的持有者,才真正地从内存中释放自己。


 

基本原则

内存管理的依循下面的基本原则

 

1、自己生成的对象,那么即是其持有者。不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)如果不想持有对象的时候,必须释放其所有权。不能释放已不再持有所有权的对象

结合 Objective-C 语言中的方法,我们来看看基本的内存管理。

方法

动作

alloc/new/copy/mutableCopy

生成对象并拥有所有权

retain

拥有对象所有权

release

释放对象所有权

dealloc

释放对象资源

 

实际上这些函数并不能说是 Objective-C 语言所特有的,而是 OS X / iOS

Objective-C 语言内部严格遵守上面表格中的定义;首先是 alloc/new/copy/mutableCopy 这几个函数,并且是alloc/new/copy/mutableCopy 开头的函数,比如:allpcMyObject/newTheObject/copyThis/mutableCopyTheObject 等都必须遵循这个原则。

反而言之,如果不是 alloc/new/copy/mutableCopy 开头的函数,而且要返回对象的话,那么调用端只是生成对象,而不是其持有者。

17.          -(id)allocObject {
18.              /*
19.               * 生成对象并拥有所有权
20.               */
21.              id obj = [[NSObject alloc] init];
22.          
23.              /*
24.               * 自己一直是持有对象状态
25.               */
26.              return obj;
27.          }

如上面的例子,alloc

再看下面的例子

•   -(id)object {
•       id obj = [[NSObject alloc] init];
•   
•       /*
•        * 自己一直是持有对象状态
•        */
•   
•       [obj autorelease];
•   
•       /*
•        * 对象还存在,只是并不持有它的所有权
•        */
•   
•       return obj;
•   }

这里我们用到了 autorelease

用 autorelease 的一个理由既是让程序员来控制对象的存活周期,而不像 C/C++ 等语言中,出栈后,栈中数据都被自动废弃,或者用 { } 框住的自动变量,当出了范围就看不到了。在 Objective-C 中,只有当 [pool drain] 被调用的时候,才清空 pool 中所有登记的对象实体,在这之前,你可以像往常一样正常使用对象。

当然可以想象得到的,如果一个程序只有一个 NSAutoreleasePool,并在

•   for (i=0; i < 100; i++) {
•       NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
•   
•       // 下面的函数由于不属于 alloc/new/copy/mutableCopy 范畴的函数,所以都使用了 autorelease
•       NSMutableArray* array = [NSMutableArray array];
•       NSString *str = [NSString stringWithFormat:@"TestCode"];
•   
•       /*
•        * 其他使用autorelease定义的对象
•        */
•       Test *test = [[[Test alloc] init] autorelease];
•   
•       // 通过下面的函数,可以随时监控pool中的对象
•       // iOS以外的运行库的情况下,也可以使用
•       // extern
•       [NSAutoreleasePool showPools];
•   
•       // 这里把所有pool中的对象都释放掉
•       [pool release];
•   }

当然 NSAutoreleasePool

编程准则

基于以上原则,在 ARC 诞生之前,我们往往用下面准则来写代码。

生成对象时,使用autorelease

一般情况下,我们这样生成对象并使用

•   MyController* controller = [[MyController alloc] init];
•   // ......
•   [controller release];

如果在 [controller release] 之前函数return了怎么样,内存泄露了;为了防患于未然,一般像下面一样 生成对象时,使用autorelease。这样一来,该对象就被自动加入到最近的那个 pool 中。

•   MyController* controller = [[[MyController alloc] init] autorelease];

 

对象代入时,先autorelease后再retain

对象代入的时候,如果之前不将变量所持有的对象释放,那么很可能引起内存泄露。比如下面的代码

•   {
•     _member = [[TempValue alloc] init];
•   }
•   
•   - (void)setValue:(TempValue *)value {
•     _member = value;
•     // 这时,之前持有的对象因为没有 release 而引起内存泄露
•     // 当然,先 [_member release] 后再代入也是可以的,
•     // 但是当与「对象在函数中返回时」的问题一同考虑时,
•     // 如果没有 return [[object retain] autorelease] 的保证,这里即使 [_member release]也是百搭
•     // 详细的解释见下
•   }

鉴于以上原因,我们将原先的对象先autorelease后再将新对象retain代入。

3. {
4.   _member = [[TempValue alloc] init];
5.   // 这里,即使使用【生成对象时,使用autorelease】的准则,也没有关系
6.   // 使用autorelease一次就将制定对象放入pool中,放几次[pool drain]的时候就释放几次
7. }
8. 
9. - (void)setValue:(TempValue *)value {
10.            [_member autorelease];
11.            _member = [value retain];
12.          }

该原则遵循 Failed Self 的原则,虽然从性能上看有所损耗但是保证了代码质量。

对象在函数中返回时

使用return [[object retain] autorelease]

严格地说,是除 alloc/new/copy/mutableCopy 开头函数以外的函数中,有对象放回时,使用return [[object retain]autorelease]。

我们结合下面的例子来说明,并总结出该问题的几种解决方案

 

12.          @implementation FooClass
13.          
14.          - (void)setObject:(MyObject *)object;
15.           {
16.               // 这里故意没有使用 autorelease,以便说明问题
17.               [_object release];
18.               _object = [object retain];
19.           }
20.          
21.          - (id)object;
22.           {
23.               return _object;
24.           }
25.          
26.          - (void)dealloc;
27.           {
28.               [_object release];
29.               [super dealloc];
30.           }
31.          
32.          @end
33.          
34.          @implementation BarClass
35.          
36.          - (void)doStuff;
37.           {
38.              FooClass * foo = [[FooClass alloc] init];
39.          
40.              // 创建第一个对象,引用计数 = 1
41.              MyObject * firstObject = [[MyObject alloc] init];
42.              // setObject中由于
43.              [foo setObject:firstObject];
44.              // 释放一次,引用计数 = 1;这之后对象有正确的所有权属性
45.              [firstObject release];
46.          
47.              // 通过非 alloc/new/copy/mutableCopy 开头函数得到对象
48.              // anObject
49.              MyObject * anObject = [foo object];
50.              [anObject testMethod];
51.          
52.              // 创建第二个对象
53.              MyObject * secondObject = [[MyObject alloc] init];
54.              // setObject中由于
55.              [foo setObject:secondObject];
56.              [secondObject release];
57.          
58.              // 程序在这里崩溃了,因为 anObject
59.              [anObject testMethod];
60.          }
61.          
62.          @end

从结论我们来看看该问题的几种可行的解决方案;各种方案中没有列出的代码与原先代码一致。

生成对象时,使用autorelease

 

16.          @implementation BarClass
17.          
18.          - (void)doStuff;
19.           {
20.              FooClass * foo = [[FooClass alloc] init];
21.          
22.              MyObject * firstObject = [[[MyObject alloc] init] autorelease];
23.              [foo setObject:firstObject];
24.          
25.              MyObject * anObject = [foo object];
26.              [anObject testMethod];
27.          
28.              MyObject * secondObject = [[[MyObject alloc] init] autorelease];
29.              [foo setObject:secondObject];
30.          
31.              [anObject testMethod];
32.          }
33.          
34.          @end

对象生成时,即被放入最近的 pool 中,不需要人为特殊的维护,对象的生命周期将被延续,出 {} 范围之时即对象释放之际。

对象代入时,先autorelease后再retain

 

•   - (void)setObject:(MyObject *)object;
•    {
•        [_object autorelease];
•        _object = [object retain];
•    }
•   
•   - (id)object;
•    {
•        // 遵循非 alloc/new/copy/mutableCopy 开头的函数,不赐予所有权原则
•        return _object;
•    }

同样的,对象被放入最近的 pool 中,第二次 setObject

对象在函数中返回时,使用return

1. - (void)setObject:(MyObject *)object;
2.  {
3.      [_object release];
4.      _object = [object retain];
5.  }
6. 
7. - (id)object;
8.  {
9.      // 遵循非 alloc/new/copy/mutableCopy 开头的函数,不赐予所有权原则
10.               return [[_object retain] autorelease];
11.           }

好不容易回到了本小节要说明的方法;可以看到这是从另一个角度解决了该问题:[foo object] 的时候保证引用计数是2,并将对象放入pool中维护。

总结上面3种方法,虽说是从不同角度入手解决了这个问题,但是基本原则不变,利用 NSAutoreleasePool

如果你觉得3种编码原则怎么搭配使用,在什么样的场合下选择比较麻烦,不要紧,都用就得了。我们牺牲的只是NSAutoreleasePool

 

 

四、ARC 诞生

ARC 是什么我不需要再解释,若有不明白,可以看看前面“ARC是什么”这个章节。

ARC 严格遵守 Objective-C 内存管理的基本原则

 

•  自己生成的对象,那么即是其持有者。

•  不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)

•  如果不想持有对象的时候,必须释放其所有权。

•  不能释放已不再持有所有权的对象

并从编译器角度维护了该原则,比如如果不是 alloc/new/copy/mutableCopy 开头的函数,编译器会将生成的对象自动放入 autoReleasePool 中。如果是 __strong 修饰的变量,编译器会自动给其加上所有权。等等,详细,我们根据不同的关键字来看看编译器为我们具体做了什么。并从中总结出 ARC 的使用规则。

__strong

我们先来看看用 __strong 修饰的变量,以及缺省隐藏的 __strong 情况。

1. {
2.     /*
3.      * 生成对象并拥有所有权
4.      */
5.     id __strong obj = [[NSObject alloc] init];
6. 
7.     /*
8.      * 自己一直是持有对象状态
9.      */
10.          }
11.              /*
12.               * 变量出生命周期时,失去全部所有者,对象内存空间被释放
13.               */

这种情况毫无悬念,缺省使用 alloc/new/copy/mutableCopy 开头的函数也是这样的结果。并且在这里,编译器帮我们自动的调用了对象的 release 函数,不需要手工维护。再看看下面的情况。

1. {
2.     /*
3.      * 生成对象但是并没有其所有权
4.      */
5.     id __strong obj = [NSMutableArray array];
6. 
7.     /*
8.      * 由于变量声明是强引用,自己一直是持有对象状态
9.      * 编译器根据函数名,再将该对象放入 autoreleasepool
10.               */
11.          }
12.              /*
13.               * 变量出生命周期时,失去全部所有者,对象内存空间被释放
14.               */

由上,虽然不是用 alloc/new/copy/mutableCopy 开头的函数得到的对象,由于是强参照,我们仍然成为对象的持有者。而这正是编译器帮我们做到的。

具体做的是什么呢?其实就是【对象在函数中返回时,使用return [[object retain] autorelease]】所描述的;如果你反汇编一下ARC生成的代码,可以看到这时会自动调用名为 objc_retainAutoreleaseReturnValue 的函数,而其作用和 [[object retain] autorelease] 一致。编译器通过函数名分析,如果不是 alloc/new/copy/mutableCopy 开头的函数,自动加入了这段代码。

另外,缺省 __strong 修饰的变量,对象代入的时候也正确地保证对象所有者规则;代入新对象时,自动释放旧对象的参照,代入nil的时候,表示释放当前对象的强参照。

__weak

虽然大部分场合,大部分问题使用 __strong 来编码就足够了;但是为了解决循环参照的问题 __weak 关键字修饰【弱参照】变量就发挥了作用。关于循环参照的问题,准备在以后的博文中介绍;今天,主要看看编译器在背后怎么处理 __weak 变量的。

__weak

当一个 __weak 所指对象被释放时,系统按下面步骤来处理

 

•  从weak表中,通过对象地址(key)找到entry

•  将entry中所有指向该对象的变量设为nil

•  从weak表中删除该entry

•  从对象引用计数表中删除对象entry(通过对象地址找到)

另外,当使用 __weak 修饰的变量的时候,变量将放入 autoreleasepool

1. {
2.     id __weak o = obj;
3.     NSLog(@"1 %@", o);
4.     NSLog(@"2 %@", o);
5.     NSLog(@"3 %@", o);
6.     NSLog(@"4 %@", o);
7.     NSLog(@"5 %@", o);
8. }

这里我们用了5次,那么pool中就被登记了5次;从效率上考虑这样当然不是很好,可以通过代入 __strong 修饰的强参照变量来避开这个问题。

1. {
2.     id __weak o = obj;
3.     id __strong temp = o;
4.     NSLog(@"1 %@", temp);
5.     NSLog(@"2 %@", temp);
6.     NSLog(@"3 %@", temp);
7.     NSLog(@"4 %@", temp);
8.     NSLog(@"5 %@", temp);
9. }

另外,还有通过重载 allowsWeakReference和retainWeakReference

话说回来,为什么使用弱参照变量的时候,要将其放入 autoreleasepool

__autoreleasing

虽然上面还没有讲到该关键字,但是编译器在很多时候已经用到了 autoreleasepool。比如非alloc/new/copy/mutableCopy 开头的函数返回一个对象的时候,又比如使用一个 __weak 声明的变量的时候。

实际上,写ARC代码的时候,明示

还有一种编译器缺省使用 __autoreleasing

1.    id *obj == id __autoreleasing *obj
2.    NSObject **obj == NSObject * __autoreleasing *obj

所以,下面两个函数的是等价的。

1. -(BOOL)performOperationWithError:(NSError **)error;
2. 
3. -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

像下面的函数调用,为什么是可行的呢?

1. NSError __strong *error = nil;
2. BOOL result = [obj performOperationWithError:&error];

其实,编译器是这样解释这段代码的。

1. NSError __strong *error = nil;
2. NSError __autoreleasing *tmp = error;
3. BOOL result = [obj performOperationWithError:&tmp];
4. error = tmp;

那么我们这样声明函数不就可以了吗?

1. -(BOOL)performOperationWithError:(NSError * __strong *)error;

答案是肯定的,你可以这样做,编译是可以通过,但你违反了非 alloc/new/copy/mutableCopy 开头的函数,不返回对象持有权的原则。这里是没有问题了,但也许影响到其他地方NG。

ARC 规则

结合上面的讲解,我想你也应该能够总结出来使用ARC的规则

 

(这里只列出本讲中涉及的内容,其他的内容以后总结)

•  代码中不能使用retain, release, retain, autorelease

•  不能使用NSAllocateObject, NSDeallocateObject

•  不能使用NSAutoReleasePool、而需要@autoreleasepool块

•  严守内存管理相关函数命名规则

关于函数命名,伴随ARC的导入,还有一系列函数的定义也被严格定义了,那就是以

1. -(id)initWithObject:(id)obj;

而下面的是NG的。

1. -(void)initWithObject;

不过声明为 -(void) initialize; 是没有问题的。

 

 

 

 

 

 

 

概念

当我们使用强参照(Strong reference)时,往往需要留意 循环参照 的问题。循环参照指的是两个对象被互相强参照,以至于任一对象都不能释放。

一般情况下,当对象之间有“父子关系”时,强参照的情况发生的比较多。比如通讯薄对象AddrBook和每个通讯录Entry的关系如下。

 

这种情况下,由于Entry对象被AddrBook强参照,所以不能释放。另一方面,如果Entry被释放了,AddrBook对象的强参照也就没有了,其对象也应被释放。

解决方式

像上面的例子,当多个对象间有“父子关系”时,需要在一侧用“弱参照”来解决循环参照问题。一般情况下,“父亲”作为“孩子”的拥有者,对“孩子”是强参照,而“孩子”对父亲是弱参照。

ARC 是什么架构_IOS_07

如图所示,当强参照AddrBook对象的变量被释放的时候,AddrBook对象将被自动释放,同时将失去Entry成员对象的强参照。另外,当AddrBook对象被释放的时候,Entry对象中的AddrBook变量也将由Zeroing机制,自动带入nil。我们不需要担心释放对象的再访问问题。

下面,我们将看看有几种情况下,需要注意循环参照问题。

Delegate模式

iOS程序中经常用到delegate模式,比如ViewController中,用ModalView打开/关闭DetailViewController时,需要delegate的设定。

ARC 是什么架构_IOS_08

 

这里,ViewController对象中强参照detailViewController,如果DetailViewController的delegate不是弱参照ViewController话,将引起循环参照。

另外,当类中使用weak

Blocks

Blocks是iOS 4开始导入的,可以理解为python或者lisp中的Lambda,C++11也已导入了该概念;类似概念ruby/smalltalk/JSP语言中也有定义。具体讲解见以后的文章,本节我们主要看看在Block中的循环参照问题。

比如,block对象用copy的属性定义时候,

22.          typedef void(^MyBlock)(void);
23.          
24.          @interface MyObject : NSObject
25.          @property (nonatomic, copy) MyBlock block;
26.          @property (nonatomic, strong) NSString *str;
27.          
28.          - (void)performBlock;
29.          @end
30.          
31.          @implementation MyObject
32.          @synthesize block, str;
33.          
34.          - (void)performBlock {
35.              if (self.block) {
36.                  self.block();
37.              }
38.          }
39.          @end

调用端如下:

28.          MyObject *object = [[MyObject alloc] init];
29.          object.str = @"hoge";
30.          
31.          object.block = ^{
32.              NSLog(@"block: str=%@", object.str);
33.          };
34.          [object performBlock];

我们看到,Block的构文中参照了object,同样object也强参照block。

ARC 是什么架构_IOS_09

为了解决该问题,我们可以有下面两种选择。

使用__block关键字修饰

使用__block关键字,让对象有读写权限,如果Block内的处理完毕就释放object。

•   __block MyObject *object = [[MyObject alloc] init];
•   object.str = @"hoge";
•   
•   object.block = ^{
•       NSLog(@"block: str=%@", object.str);
•       object = nil;
•   };
•   [object performBlock];

该关键字的意思就是让block取消对object的强参照,以避免循环参照。但是,有一个问题就是,object的释放动作是在Block内部执行,如果Block没有被执行的话,循环参照一直存在。比如上面的代码,如果第8行 [object performBlock]; 没有执行的话,那么一直还是循环参照状态。

使用__weak关键字修饰

另一种方案就是让Block的参照变为弱参照。

•   MyObject *object = [[MyObject alloc] init];
•   object.str = @"hoge";
•   
•   __weak MyObject *weakObject = object;
•   object.block = ^{
•       NSLog(@"block: str=%@", weakObject.str);
•   };
•   [object performBlock];

考虑到异步通信时Blocks的使用情况,weak变量weakObject有可能随时变为nil,所以类似于下面先变为strong变量,并检查是否为nil的处理方式应该更安全。

•   MyObject *object = [[MyObject alloc] init];
•   object.str = @"hoge";
•   
•   __weak MyObject *weakObject = object;
•   object.block = ^{
•       MyObject strongObject = weakObject;
•       if (strongObject) {
•           NSLog(@"block: str=%@", strongObject.str);
•       }
•   };
•   [object performBlock];

总上,当我们使用Blocks时,也需要考虑Block中变量和实例的关系,不要引起不必要的循环参照问题。