第05章 基于引用计数的内存管理 下
5.5 循环引用和弱引用
5.5.1 循环引用
举例如下:
#import <Foundation/Foundation.h>
@interface MRCTest : NSObject
@property (nonatomic,retain) MRCTest *relatedMRCTest;
-(void)MRCTestFun;
@end
#import "MRCTest.h"
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@implementation MRCTest
-(void)MRCTestFun
{
MRCTest *testA = [[MRCTest alloc]init];
MRCTest *testB = [[MRCTest alloc]init];
testA.relatedMRCTest = testB;
testB.relatedMRCTest = testA;
NSLog(@"%u %u",testA.retainCount,testB.retainCount);//2016-06-03 11:01:35.019 Test[716:11718] 2 2
[testA release];
[testB release];
NSLog(@"%u %u",testA.retainCount,testB.retainCount);//2016-06-03 11:01:42.219 Test[716:11718] 1 1
}
@end
像这种两个对象互相引用,或者像A持有B、B持有C、C持有A这样多个对象的引用关系形成了环的现象,叫做循环引用或循环保持(retain cycle)。循环引用会造成内存泄漏,只有打破循环引用关系才能够释放内存。
5.5.2 所有权和对象间的关系
拥有多有权的实例变量和指通过指针指向,不拥有所有权的变量在内存方面的处理截然不同,前者要注意循环引用之类的情景造成的内存泄漏,后者要注意当你使用对象时,对象可能已经在不知不觉中被释放了。
5.5.3 弱引用
ARC中引入了弱引用(weak reference)的概念,如下:
__weak id temp;
__weak NSObject *cacheObj;
声明变量时,__weak或者__strong可以出现在声明中的任意位置:
__weak NSObject *a,*b;
NSObject __weak *c,*d;
NSObject * __weak e,*__weak f; //最后一个f前面的__weak不能省略。
强引用和弱引用变量都会被隐式地初始化为nil。
这种用于修饰指针类型变量的修饰符叫做生命周期修饰符(lifetime qualifier(|ˈkwɒlɪfaɪə(r)| noun 修饰词 ))或所有权修饰符。这种生命周期修饰符一共有四种:__strong、__weak、__autoreleasing、__unsafe_untrained.
5.5.4 自动nil化的弱引用
弱引用会在其指向的实例对象被释放后自动变成nil,这就是弱引用的自动nil化功能。也就是说,即使弱引用指向的实例对象在不知不觉中被释放了,弱引用也不会变成野指针。
在ARC中只有强引用才能改变对象的引用计数,保持住对象,因此,如果想保持住某个对象,至少要为其赋值一个强引用类型的变量。下面是一个极端的例子,因为赋值给了一个弱引用,所以生成的对象立刻被释放了
__weak People *w = [[People alloc] init];
5.5.5 对象之间引用关系的基本原则
面向对象的编程中,一个对象的实例变量引用着另外的对象,对象之间是通过引用连接在一起的。如果把这些关系画出来,就可能得到一张图,因此,我们就把这些对象和他们之间的关系称为对象图(object graph)。在对象图中,一个对象可能被多个对象引用,对象之间的引用关系可能存在环路。
对象图中的环路就是循环引用产生的原因。使用ARC的时候应该尽量保证对象之间的关系呈树形结构,如果必须要两个对象之间有相互引用的情况,可以使用弱引用来解决
5.6 ARC编程时其他一些注意事项
5.6.1 可以像通常的指针一样使用的对象
使用ARC的时候,如果即不想保持赋值的对象,也不想赋值的对象在释放后被自动设为nil,可以使用生命周期修饰符__unsafe_unretained。__unsafe_unretained所修饰的变量称为非nil化的弱指针,也就是说,如果所指向的内存区域被释放了,这个指针就是一个野指针了。
iOS的世界中有两种对象:Objective-C对象和Core Foundation对象。其中,Core Foundation类型的对象不再ARC的管理范畴内。因此,当转换这两种类型时,就要告诉编译器怎样处理对象的所有权。
基于相同的原因,在ARC程序中,就算加上__unsafe_unretained上修饰符,id类型和void*类型之间也是不能进行转换的。可以使用_bridge 修饰符来实现它们之间的相互转换。
5.6.2 setter 方法的注意事项
5.6.3 通过函数的参数返回结果对象
当一个函数或者方法有多个返回值的时候,我们可以通过函数或方法的参数传入一个指针,将返回值写入指针所指向的空间。C语言中把这种方法叫做按引用传递(pass-by-reference)。Objective-C的ARC中也有类似的方法,但采用了和C语言不同的实现方法,叫做写回传(pass-by-writeback)。
下面的例子有一个难点就是为什么要传二重指针,个人理解如下:
1:为什么要传递指针:
函数的参数传递是通过压栈传递的,与原有的变量并不是同一个变量,在函数内部即使对传递的参数做了修改,也只是修改了栈中的数据。所以C中通过传递指针,修改指针指向的的内容的方法来达到回传数据的目的。
2:普遍的写法是先声明一个NSError *err = nil;
如果直接传递err,则处理完成后err依然指向空,没有任何用处。
3:可以先开辟一个NSError,如NSError *err = [[NSError alloc] init]; 然后再传递给方法,让方法中修改err的属性,达到回传数据的目的,但这样有一个问题,错误类型可能是NSError的子类。无法预先猜测err的具体类型。
以 以下方法 为例:
-(id)initWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error;
ARC的编译器会自动为函数的二重指针变量加上__autoreleasing修饰符。在ARC有效的情况下,上面声明的函数可以被编译为:
-(id)initWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(__autoreleasing NSError **)error;
__autoreleasing 的根本目的就是获得一个延迟释放的对象。
调用上述方法的代码可能如下:
NSError *error = nil;
NSString *string = [[NSString alloc] initWithContentsOfFile:@“/path/to/file.txt” encoding NSUTF8StringEncoding error:&error];
编译器会把这段代码转换为:
NSError __string *error = nil;
NSError __autoreleasing *tempError = error;
NSString *string = [[NSString alloc] initWithContentsOfFile:@“/path/to/file.txt” encoding NSUTF8StringEncoding error:& tempError];
error = tempError;
5.6.4 C语言数组保存Objective-C对象
ARC有效的程序中可以用C语言保存Objective-C的对象,但代码又臭又长,完全不是好主意。
5.6.5 ARC对结构体的一些限制
ARC有效的情况下,不可以在C语言的结构体或共用体中定义Objective-C的对象。原因是编译器不能自动释放结构体(或共用体)内部的Objective-C对象。
会报
error: ARC forbids Objective-C obj in structs or unions。
一种常见的做法是使用Objective-C的类来代替结构体或共用体。如果实在是因为效率或其它原因无论如何都要使用结构体的话,可以使用__unsafe_unretained修饰符来修饰结构体中的Objective-C变量。这样一来,编译器就不会管理这个变量的内存,所以需要完全手动的管理内存。
5.6.6 提示编译器进行特别处理
未使用ARC的时候你可能没有按照ARC中的命名规则来为方法起名,而这种情况下如果因为某些原因没法修改这些方法的名字,那么将这些代码迁移到ARC环境中就会有问题。这是可以通过给方法加上事先定义好的一些宏来告诉编译器应该如何对这个方法的返回值进行内存管理。
如:
NS_RETURNS_RETAINED
指明这个方法和init或copy开头的方法一样,由调用端负责释放返回的对象。
NS_RETURNS_NOT_RETAINED
指明这个方法不属于内存管理方面的方法,调用端无需释放返回的对象。
如下方法
+ (FinishingDate*)newMoon NS_RETURNS_NOT_RETAINED;
虽然他是一个以new开头的方法,但是通过后面的宏来告诉编译器,这是方法的返回值不需要调用端释放(详情见~中)。
编译时,这些宏会被替换为注释(annotation)。这些宏被定义在了NSObjCRuntime.h中
如:
-(MRCTest*)NewMRCTestFun NS_RETURNS_NOT_RETAINED
通过Preprocess后:
-(MRCTest*)NewMRCTestFun __attribute__((ns_returns_not_retained))
也可以通过条件编译确定ARC是否有效
编译条件是:
#if __has_feature(objc_arc)
如果为真,则表明ARC有效,否则就表示ARC无效。