iPhone开发深入浅出 — ARC
本文摘自“泰然”论坛
一、ARC是什么
ARC是iOS 5推出的新功能,全称叫 ARC(Automatic Reference Counting)。简单地说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。
该机能在 iOS 5/ Mac OS X 10.7 开始导入,利用 Xcode4.2 可以使用该机能。简单地理解ARC,就是通过指定的语法,让编译器(LLVM 3.0)在编译代码时,自动生成实例的引用计数管理部分代码。有一点,ARC并不是GC,它只是一种代码静态分析(Static Analyzer)工具。
变化点
通过一小段代码,我们看看使用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就是 ON 的状态,所以编译旧代码的时候往往有”Automatic Reference CountingIssue”的错误信息。
这个时候,可以将项目编译设置中的“Objectice-C Auto Reference Counteting”设为NO。如下所示。
如果只想对某个.m文件不适应ARC,可以只针对该类文件加上 -fno-objc-arc 编译FLAGS,如下图。
ARC基本规则
• retain, release, autorelease, dealloc由编译器自动插入,不能在代码中调用.
• dealloc虽然可以被重载,但是不能调用[super dealloc].
由于ARC并不是GC,并需要一些规则让编译器支持代码插入,所以必须清楚清楚了这些规则后,才能写出健壮的代码。
Objective-C对象
ObjectiveC中的对象,有强参照(Strong reference)和弱参照(Weak reference)之分,当需要保持其他对象的时候,需要retain以确保对象引用计数加1。对象的持有者(owner)只要存在,那么该对象的强参照就一直存在。
对象处理的基本规则是
• 只要对象的持有者存在(对象被强参照),那么就可以使用该对象
• 对象失去了持有者后,即被破弃强参照 (Strong reference)
(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) 的使用方式。
(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 5/ Mac OS X 10.7 开始导入。由于该类型不影响对象的生命周期,所以如果对象之前就没有持有者,那么会出现刚创建就被破弃的问题,比如下面的代码。
1. NSString __weak *string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];
2. NSLog(@"string: %@", string); //此时 string为空
如果编译设定OS版本 Deployment Target 设定为这比这低的版本,那么编译时将报错(The current deployment target does not support automated __weak references),这个时候,我们可以使用下面的__unsafe_unretained。
弱参照还有一个特征,即当参数对象失去所有者之后,变量会被自动赋值上nil (Zeroing)。
__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”开始的属性名称 。(如果使用会有下面的编译错误”Property’s synthesized getter follows Cocoa naming convention for returning ‘owned’ objects”)
今后,我们将更加深入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 和 Outlet 应该用 weak 属性来声明。同时,如上一回介绍的 iOS 5 之前的版本是没有 __weak 关键字的,所以 weak 属性是不能使用的。这种情况我们使用 unsafe_unretained。
unsafe_unretained
等效于__unsafe_unretaind关键字声明的变量;像上面说明的,iOS 5之前的系统用该属性代替 weak 来使用。
copy
与 strong 的区别是声明变量是拷贝对象的持有者。
assign
一般Scalar Varible用该属性声明,比如,int, BOOL。
retain
该属性与 strong 一致;只是可读性更强一些。
读写相关的属性 (readwrite, readonly)
读写相关的属性有 readwrite 和 readonly 两种,如果使用ARC之后,我么需要注意一下 readonly 属性的使用。
比如下面的变量声明。
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 编译器在背后为我们做了什么。
Objective-C 内存管理
和许多面向对象语言一样,Objective-C 中内存管理的方式其实就是指 引用计数 (Reference Counting)的使用准则。如下图所示,对象生成的时候必定被某个持有者拿着,如果有多个持有者的话,其引用计数就会递增;相反失去一个持有者那么引用计数即会递减,直到失去所有的持有者,才真正地从内存中释放自己。
基本原则
内存管理的依循下面的基本原则
1、自己生成的对象,那么即是其持有者。不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)如果不想持有对象的时候,必须释放其所有权。不能释放已不再持有所有权的对象
结合 Objective-C 语言中的方法,我们来看看基本的内存管理。
方法 |
动作 |
alloc/new/copy/mutableCopy |
生成对象并拥有所有权 |
retain |
拥有对象所有权 |
release |
释放对象所有权 |
dealloc |
释放对象资源 |
实际上这些函数并不能说是 Objective-C 语言所特有的,而是 OS X / iOS 系统库中包含的基类函数;具体说就是 Cocoa Framework::Foundation::NSObject 基类的成员函数。
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 生成的对象,其所有权会传递给函数的调用端;即满足了 alloc 开头函数的命名规则。
再看下面的例子
• -(id)object {
• id obj = [[NSObject alloc] init];
•
• /*
• * 自己一直是持有对象状态
• */
•
• [obj autorelease];
•
• /*
• * 对象还存在,只是并不持有它的所有权
• */
•
• return obj;
• }
这里我们用到了 autorelease 函数。它的作用既是将对象放入 NSAutoreleasePool 中,由其来维护其生命周期。换句话说对象的持有者是 NSAutoreleasePool;上面的例子中,object 返回后,调用者将不持有其所有权。(除非再调用 retain。)
用 autorelease 的一个理由既是让程序员来控制对象的存活周期,而不像 C/C++ 等语言中,出栈后,栈中数据都被自动废弃,或者用 { } 框住的自动变量,当出了范围就看不到了。在 Objective-C 中,只有当 [pool drain] 被调用的时候,才清空 pool 中所有登记的对象实体,在这之前,你可以像往常一样正常使用对象。
当然可以想象得到的,如果一个程序只有一个 NSAutoreleasePool,并在 main 中声明,程序结束时才 [pool drain]/[pool release] 的话,那么所有 autorelease 的对象都将塞满这个 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以外的运行库的情况下,也可以使用 _objc_autoreleasePoolPrint() 私有函数,只是需要下面的声明
• // extern void _objc_autoreleasePoolPrint();
• [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中由于 [object retain] ,引用计数 = 2
43. [foo setObject:firstObject];
44. // 释放一次,引用计数 = 1;这之后对象有正确的所有权属性
45. [firstObject release];
46.
47. // 通过非 alloc/new/copy/mutableCopy 开头函数得到对象
48. // anObject 指向第一个对象,但是并没有其所有权,对象引用计数 = 1
49. MyObject * anObject = [foo object];
50. [anObject testMethod];
51.
52. // 创建第二个对象
53. MyObject * secondObject = [[MyObject alloc] init];
54. // setObject中由于 [_object release]; 第一个对象引用计数 = 0,内存被释放
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 后对象引用计数仍为1, pool 清空时才执行最后一次对象release,从而保证了代码的正确性。
对象在函数中返回时,使用return [[object retain] autorelease];
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表中,该表和引用计数的表格类似,是一个Hash表,都是以对象的内存地址做key,同时,针对一个对象地址的key,可以同时对应多个变量的地址。
当一个 __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 函数来限制 __weak 声明变量使用回数的方法,毕竟不在本次讨论范畴之内,就此省略。
话说回来,为什么使用弱参照变量的时候,要将其放入 autoreleasepool 中呢?想想弱参照的定义就应该明白了 —- 如果在访问弱参照对象时,该对象被释放了怎么办,程序不就崩溃了;所以为了解决该问题,又再一次用到了 pool。
__autoreleasing
虽然上面还没有讲到该关键字,但是编译器在很多时候已经用到了 autoreleasepool。比如非alloc/new/copy/mutableCopy 开头的函数返回一个对象的时候,又比如使用一个 __weak 声明的变量的时候。
实际上,写ARC代码的时候,明示 __autoreleasing 声明变量和明示 __strong 声明变量一样基本上没有,因为编译器已经为我们做了很多,很智能了(前提是我们要按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的导入,还有一系列函数的定义也被严格定义了,那就是以 init 开头的函数。init 函数作为alloc生成对象的初期化函数,需要按原样直接传递对象给调用段,所以下面的声明是OK的。
1. -(id)initWithObject:(id)obj;
而下面的是NG的。
1. -(void)initWithObject;
不过声明为 -(void) initialize; 是没有问题的。
概念
当我们使用强参照(Strong reference)时,往往需要留意 循环参照 的问题。循环参照指的是两个对象被互相强参照,以至于任一对象都不能释放。
一般情况下,当对象之间有“父子关系”时,强参照的情况发生的比较多。比如通讯薄对象AddrBook和每个通讯录Entry的关系如下。
这种情况下,由于Entry对象被AddrBook强参照,所以不能释放。另一方面,如果Entry被释放了,AddrBook对象的强参照也就没有了,其对象也应被释放。
解决方式
像上面的例子,当多个对象间有“父子关系”时,需要在一侧用“弱参照”来解决循环参照问题。一般情况下,“父亲”作为“孩子”的拥有者,对“孩子”是强参照,而“孩子”对父亲是弱参照。
如图所示,当强参照AddrBook对象的变量被释放的时候,AddrBook对象将被自动释放,同时将失去Entry成员对象的强参照。另外,当AddrBook对象被释放的时候,Entry对象中的AddrBook变量也将由Zeroing机制,自动带入nil。我们不需要担心释放对象的再访问问题。
下面,我们将看看有几种情况下,需要注意循环参照问题。
Delegate模式
iOS程序中经常用到delegate模式,比如ViewController中,用ModalView打开/关闭DetailViewController时,需要delegate的设定。
这里,ViewController对象中强参照detailViewController,如果DetailViewController的delegate不是弱参照ViewController话,将引起循环参照。
另外,当类中使用weak @property声明的delegate变量时,如果参照对象被释放,该变量将被自动设为nil,不需要程序代码设置。
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。
为了解决该问题,我们可以有下面两种选择。
使用__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中变量和实例的关系,不要引起不必要的循环参照问题。