Objective-C高级编程读书笔记:自动引用计数(ARC)

1. 所有权修饰符

ARC有效时,id类型和对象类型上必须附加所有权修饰符:

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing
__strong

__strong修饰符是id类型和对象类型的默认所有权修饰符,在没有指定明确的所有权修饰符时,默认为__strong修饰符。

id obj = [[NSObject alloc] inti];
//等效于
id __strong obj = [[NSObject alloc] inti];

__strong表示对对象的强引用,用它修饰的变量在超出作用域时,会释放其被赋予的对象。

{
    //自己生成并持有对象
    id __strong obj = [[NSObject alloc] init];
}
    //变量obj超出其作用域,强引用失效,自动释放自己持有的对象,
    //对象的持有者不存在,废弃该对象。

__strong修饰的变量,在赋值上也能正确地管理其对象的所有者。

id __strong obj0 = [[NSObject alloc] init];    //对象A的持有者为obj0
id __strong obj1 = [[NSObject alloc] init];    //对象B的持有者为obj1
id __strong obj2 = nil;
obj0 = obj1;    //对象A的持有者不存在,废弃对象A。对象B的持有者变为obj0和obj1。
obj2 = obj0;    //对象B的持有者变为obj0,obj1和obj2。
obj1 = nil;    //对象B的持有者变为obj0和obj2。
obj0 = nil;    //对象B的持有者变为obj2。
obj2 = nil;    //对象B的持有者不存在,废弃对象B。

通过__strong修饰符,不必再次键入retain和release。对带__strong修饰符的变量赋值就可以持有对象,废弃带__strong修饰符的变量或对变量赋值就可以释放对象。

__weak

仅使用__strong修饰符,有些问题无法解决,那就是引用计数式内存管理中必然会发生的循环引用问题。__strong修饰符的成员变量在持有对象时,很容易发生循环引用。

@interface Test : NSObject
{
    id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end

@implementation Test
- (id)init
{
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj
{
    obj_ = obj;
}
@end

//以下为循环引用
{
    id obj0 = [[Test alloc] init];    //对象A
    id obj1 = [[Test alloc] init];    //对象B
    [obj0 setObject:obj1];
    [obj1 setObject:obj0];
}
//虽然obj0和obj1超出作用域后释放了各自持有的对象,
//但是还有对象A的obj_强引用对象B,对象B的obj_强引用对象A,
//所以A和B不会被废弃,发生了内存泄漏。

这时候就要使用__weak修饰符,它与__strong相反,提供弱引用,不能持有对象实例。将上面的代码修改一下就不会发生循环引用了:

@interface Test : NSObject
{
    id __weak obj_;
}
- (void)setObject:(id _strong)obj;
@end

__weak修饰符还有一个优点,在持有某个对象的弱引用时,若对象被废弃,此弱引用将自动被赋值为nil,通过检查__weak修饰的变量是否为nil可以判断对象是否已废弃。

__unsafe_unretained

顾名思义,__unsafe_unretained是不安全的所有权修饰符,尽管ARC式的内存管理是编译器的工作,但__unsafe_unretained修饰的变量不是编译器的内存管理对象。__unsafe_unretained修饰的变量和__weak修饰的变量一样都不持有对象,区别是它不会像__weak修饰的变量一样在对象被废弃时赋值为nil,所以可能形成野指针。

id __unsafe_unretained obj0 = nil;

{
    //自己生成并持有对象
    id __strong obj1 = [NSObject alloc] init]; 
    //虽然obj1赋值给了obj0,但obj0并不持有对象
    obj0 = obj1;
    NSLog(@"A: %@", obj0);
}
//obj1超出作用域,强引用失效,释放自己持有的对象,对象无持有者,废弃该对象,
//所以obj0表示的对象已被废弃,obj0变成野指针。 
NSLog(@"A: %@", obj0);
__autoreleasing

ARC下不能使用autorelease方法,也不能使用NSAutoreleasePool类,但实际上autorelease功能是起作用的。使用@autoreleasepool块替代NSAutoreleasePool类,用__autoreleasing修饰的变量替代autorelease方法:

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

但是,显示地附加__autoreleasing和显示地附加__strong一样罕见。取得非自己生成并持有的对象时,对象已经被自动注册到autoreleasepool,因为编译器会检查方法名是否以alloc/new/copy/mutableCopy开头,如果不是则自动将返回值的对象注册到autoreleasepool。另外,init方法返回的对象不注册到autoreleasepool。

@autoreleasepool {
    //取得非自己生成并持有的对象
    id __strong obj = [NSMutableArray array];
    /*
    由于obj是强引用,所以自己持有对象,
    并且编译器在判断方法名后,将对象注册到autoreleasepool
    */
}
/*
obj超出作用域,自动释放持有的对象
同时,autoreleasepool块结束,注册到其中
的所有对象自动释放。
对象的持有者不存在,废弃对象。
*/

访问__weak修饰的变量时,实际上必定要访问注册到autoreleasepool的对象。因为__weak修饰的变量只持有对象的弱引用,在访问对象的过程中,该对象有可能被废弃,注册到autoreleasepool,那么在@autoreleasepool块结束前都能确保该对象存在。

id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);

等同于

id __weak obj1 = obj0;
id __autoreleasing temp = obj1;
NSLog(@"class = %@", [temp class]);

id的指针和对象的指针在没有指定所有权修饰符时,会被加上__autoreleasing修饰符。比如,为了得到详细的错误信息,经常会在方法的参数中传递NSError对象的指针,而不是返回值。

- (BOOL)performOperationWithError:(NSError **)error;
//等同于
- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

该方法使用如下:

NSError *error = nil;
BOOL result = [obj performOperationWithError:&error];

使用该方法发生错误应该是这样:

- (BOOL)performOperationWihError:(NSError * __autoreleasing *)error
{
    //发生错误
    *error = [NSError errorWithDomain:MyAppDomain error:errorCode userinfo:nil];
    return NO;
}

需要注意的是,赋值给对象指针时,所有权修饰符必须一致:

NSError *error0 = nil;
NSError **pError0 = &error0;
//编译错误

NSError *error1 = nil;
NSError * __strong *pError1 = &error1;
//编译正常

NSError __weak *error2 = nil
NSError * __weak *pError2 = &error2;
//编译正常

2. ARC规则

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 须遵守内存管理的方法命名规则
  • 不要显示调用dealloc
  • 使用@autoreleasepool块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体(struct/union)的成员
  • 显示转换id和void *

参考:《Objective-C高级编程 iOS与OS X多线程和内存管理》




上一篇:RedMonk 2020 年 Q1 季度的编程语言排行榜前 20 名


下一篇:RedMonk 编程语言排名:Objective-C 升至第九