Objective-C 内存管理之ARC规则

ARC为自动引用计数,引用计数式内存管理的本质并没有改变,ARC只是自动处理“引用计数”的相关部分。

在编译上,可以设置ARC有效或无效。默认工程中ARC有效,若设置无效,则指定编译器属性为 -fno-objc-arc

内存管理思考方式

同引用计数相同

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 自己持有的对象不需要时释放
  • 非自己持有的对象无法释放

所有权修饰符

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__stong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符。 也就是说,以下代码中的id变量实际上都被家了所有权修饰符

id __strong obj = [[NSObject alloc] init];

在ARC无效时,写法虽然同ARC有效时一致,但

{
    id __strong obj = [[NSObject alloc] init];
}

以上代码明确指定了变量的作用域。

ARC无效时,该源码可记述如下:

/** ARC无效 */
{
    id obj =[[NSObject alloc] init];
    [obj release];
}

为了释放生成并持有的对象,增加调用了release方法的代码。该源代码进行的动作跟ARC有效时的动作完全一样。

如上所示,附有__strong修饰符的变量obj在超出其变量作用域是,即在该变量被废弃是,会释放其被赋予的对象。

__strong修饰符表示对对象的“强引用”。持有强你用的变量在超出其作用域时被废弃,虽则会强引用的失效,引用的对象会随之释放。

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

取得非自己生成并持有的对象时,代码如下:

/**取得非自己生成并持有的对象*/
{
    id __strong obj = [NSMutableArray array];
    /**因为变量obj为强引用,所以自己持有对象*/
}
/**因为变量obj超出其作用范围,强引用失效,所以自动地释放自己持有的对象*/

变量的赋值如下:

id __strong obj0 = [[NSObject alloc] init]; /**对象A*/
/**obj0持有对象A的强引用*/

id __strong obj1 = [[NSObject alloc] init]; /**对象B*/
/**obj1持有对象B的强引用*/

id __strong obj2 = nil;
/**obj2不持有任何对象*/

obj0 = obj1;
/*
 *obj0持有由obj1赋值的对象B的强引用
 *因为obj0被赋值,所以原先持有对象A的强引用失效。
 *对象A的所有者不存在,因为废弃对象A.
 *此时,持有对象B的强引用的变量为obj0和obj1
 */
 
obj2 = obj0;
/*
 *obj2持有由obj0赋值的对象B的强引用
 *此时,持有对象B的强引用的变量为obj0,obj1和obj2;
 */
 
obj1 = nil;
/*
 *因为nil被赋予了obj1,所以对对象B的强引用失效。
 *此时,持有对象B的强引用的变量为obj0和obj2;
 */
 
obj0 = nil;
/*
 *因为nil被赋予了obj1,所以对对象B的强引用失效。
 *此时,持有对象B的强引用的变量为obj0和obj2;
 */
 
obj2 = nil;
/*
 *因为nil被赋予了obj2,所以对对象B的强引用失效。
 *因为对象B的所有者不存在,所以废弃对象B.
 */

__strong修饰符的变量,不仅只在变量作用域中,在赋值上也能够正确得管理其对象的所有者。同样,在方法的参数上,也可以使用附有__strong修饰符的变量。

@interface Test : NSObjecgt
{
    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 __strong test = [[Test alloc] init];
    /*
     *test 持有Test对象的强引用
     */
    [test setObject:[[NSObject alloc] init];
    /*
     *Test对象的obj_成员,
     *持有NSObject对象的引用;
     */
}
/*
 *因为test变量超出其作用域,强引用失效,所以自动释放Test对象。
 *Test对象的所有者不存在,因此废弃该对象。
 *废弃Test对象的同时,Test的成员变量obj_也被废弃,
 *NSObject对象的强引用失效,自动释放NSObject对象。
 *NSObject对象的所有者不存在,因此废弃该对象。
 */

另外,__strong修饰符同__weak __autoreleasing,可以保证将附有这些修饰符的自动变量初始化为nil。

通过__strong修饰符,不必再次retain或者release,就可以满足引用计数内存管理的思考方式。

  • 自己生成的对象自己持有
  • 非自己生成的对象,自己也可以持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

__weak修饰符

仅仅通过__strong修饰符不能够解决引用计数内存管理中的“循环引用”问题。

例如:

@interface Test : NSObjecgt
{
    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 test0 = [[Test alloc] init];/* 对象A */
    /*
     * test0 持有Test对象A的强引用
     */
     
    id test1 = [[Test alloc] init];/* 对象B */
    /*
     * test1 持有Test对象B的强引用
     */
     
    [test0 setObject:test1];
    /*
     * Test对象A的obj_成员变量持有Test对象B的强引用
     * 此时,持有Test对象B的强引用的变量为Test对象A的obj_和test1
     */
     
    [test1 setObject:test0];
    /*
     * Test对象B的obj_成员变量持有Test对象A的强引用
     * 此时,持有Test对象A的强引用的变量为Test对象B的obj_和test0
     */
     
}
/*
 * 因为test0变量超出其作用域,强引用失效,所以自动释放Test对象A
 * 因为test1变量超出其作用域,强引用失效,所以自动释放Test对象B
 * 此时持有Test对象A的强引用的变量为Test对象B的obj_
 * 此时持有Test对象B的强引用的变量为Test对象A的obj_
 * 发生内存泄漏
 */

循环引用容易发生内存泄漏。内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。

以下代码也会引起内存泄漏(对自身的强引用)

id test = [[Test alloc] init];
[test setObject:test];

使用__weak修饰符可以避免循环引用。

__weak修饰符与__strong修饰符相反,提供弱引用。弱引用不能持有对象实例。

id __weak obj = [[NSObject alloc] init];

如果运行以上代码,编译器会发出警告。

Assigning retained object to weak variable; object will be released after assignment

以上代码将自己生成并持有的对象赋值给附有__weak修饰符的变量obj。即变量obj持有对持有对象的弱引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会被立即释放。如果将对象赋值给附有__strong修饰符的变量之后再赋值给附有__weak修饰符的变量,就不会发生警告了。

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

因为带__weak修饰符的变量(弱引用)不持有对象,所以在超出其变量作用域时,对象即被释放。如下代码即可避免循环引用。

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

__weak修饰符还有另一优点:在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态(空弱引用)。如下所示:

id __weak obj1 = nil;
{
    /**自己生成并持有对象*/
    
    id __strong obj0 = [[NSObject alloc] init];
    
    /**因为obj0变量为强引用,所以自己持有对象*/
    
    obj1 = obj0;
    
    /**obj1持有对象的弱引用*/
    
    NSLog(@"A: %@",obj1);
    /** 输出obj1变量持有的弱引用的对象*/
}
/*
 *因为obj0变量超出其作用域,强引用失效,所以自动释放自己持有的对象。
 *因为对象无持有者,所以废弃该对象。
 *废弃对象的同时,持有该对象弱引用的obj1变量的弱引用失效,nil赋值给obj1.
 */

NSLog(@"B: %@",obj1);
/** 输出赋值给obj1变量中的nil */

该代码的运行结果为:

2017-12-05 20:13:28.458858+0800 ImageOrientation[6316:1604800] A: 
2017-12-05 20:13:30.112086+0800 ImageOrientation[6316:1604800] B: (null)

以上,使用__weak修饰符可以避免循环引用。通过检查__weak修饰符的变量是否为nil,可以判断被赋值的对象是否以废弃。

__unsafe_unretained 修饰符

__unsafe_unretained修饰符的变量部署与编译器的内存管理对象。(ARC式的内存管理式编译器的工作)

id __unsafe_unretained obj = [[NSObject alloc] init];

如果运行以上代码,编译器会发出警告。虽然使用了unsafe的变量,但是编译器并不会忽略。

Assigning retained object to unsafe_unretained variable; object will be released after assignment

附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即释放。

id __unsafe_unretained obj1 = nil;
{
    /**自己生成并持有对象*/
    
    id __strong obj0 = [[NSObject alloc] init];
    
    /**因为obj0变量为强引用,所以自己持有对象*/
    
    obj1 = obj0;
    
    /**虽然obj0变量赋值给obj1,但obj1变量既不持有队形的强引用,也不持有对象的弱引用*/
    
    NSLog(@"A: %@",obj1);
    /** 输出obj1变量表示的对象*/
}
/*
 *因为obj0变量超出其作用域,强引用失效,所以自动释放自己持有的对象。
 *因为对象无持有者,所以废弃该对象。
 */

NSLog(@"B: %@",obj1);
/*
 * 输出obj1变量表示的对象
 *
 * obj1变量表示的对象已经被废弃(悬垂指针)
 * 指向曾经存在的对象,但该对象已经不再存在了,此类指针称为悬垂指针。结果未定义,往往导致程序错误,而且难以检测。
 * 错误访问
 */

该代码的运行结果为:

2017-12-06 08:45:54.005966+0800 ImageOrientation[6859:1736666] A: 

运行到NSLog(@"B: %@",obj1)时crash:Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

__autoreleasing 修饰符

ARC有效时,不能使用autorelease方法,也不能使用NSAutoreleasePool类。虽然autorelease无法直接使用,但是autorelease功能是起作用的。

ARC无效时代码如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autoreleae];
[pool drain];

ARC有效时,代码可以写成下面这样:

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

指定“@autoreleasepool块”来代替“NSAutoreleasePool类对象生成,持有以及废弃”这一范围。

另外,ARC有效时,要通过将对象赋值给附加了 __autoreleaing修饰符的变量来代替调用autorelease方法。对象赋值给附有__autoreleasing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,即对象被注册到autoreleasepool。

也就是说可以理解为,在ARC有效时,用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法。

一般来说,不会显示地附加__autoreleasing修饰符。在取得非自己生成并持有的对象时,索然可以使用alloc/new/copy/mutableCopy以外的方法来取得对象,但该对象已经被注册到了autoreleasepool。这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool。(init方法返回值的对象不注册到autoreleasepool)

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

访问附有__weak修饰符的变量时,必须访问注册到autoreleasepool的对象。

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

等同于

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

因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果要把访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存爱。因此,在使用附有__weak修饰符的变量时就必定要shying注册到autoreleasepool中的对象。

同样地,id的指针或者对象的指针在没有显示指定时会被附加上__autoreleasing修饰符。

ARC 规则

在ARC有效的情况下编译源代码,必须遵守以下规则:

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

不能使用retain/release/retainCount/autorelease

内存管理是编译器的工作,因为没有必要使用内存管理的方法(retain/release/retainCount/autorelease)。只能在ARC无效且手动进行内存管理时才能使用。

不能使用NSAllocateObject/NSDeallocateObject

一般通过调用NSObject类的alloc类方法来生成并持有OC对象。alloc类方法实际上是通过直接调用NSAllocateObject函数来生成并持有对象的。

须遵守内存管理的方法命名规则

在ARC无效时,用于对象生成/持有的方法必须遵循以下命名规则:以alloc/new/copy/mutablCopy开始的方法在返回对象时,必须返回给调用方所应当持有的对象。

在ARC有效时,除了上述规则外,增加init规则。

以init开始的方法的规则要比alloc/new/copy/mutableCopy更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应该为id类型或者该方法声明类的对象类型。

id obj = [[NSObject alloc] init];

如上所示,init方法会初始化alloc方法返回的对象,然后原封不动地返还给调用方。

注:initialize不包含在上述命名规则里。

不要显式调用dealloc

无论ARC是否有效,只要对象的所有者都不持有该对象,该对象就被废弃。对象被废弃时,无论ARC是否有效,都会调用对象的dealloc方法。在ARC无效时,必须调用 [super dealloc]
。ARC有效时会遵循无法显式调用dealloc这一规则,ARC对此会自动进行处理,dealloc中只需技术废弃对象时所必须的处理,比如删除已注册的代理或者观察者对象。

使用@autoreleasepool块代替NSAutoreleasePoll

不能使用区域(NSZone)

对象型变量不能作为C语言结构体的成员

struct Data{
    NSMutableArray * array;
}

编译后

error:ARC forbids Objective-C objects in struct

因为C语言的规约上没有方法来管理结构体成员的生存周期。

要把对象型变量假如到结构体成员中,可强制转换为void*或者是附加__unsafe_unretained修饰符。(附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象)

显式转换id和void*

ARC无效时,id变量和void*变量相互赋值没有问题,但是ARC有效时则会引起编译错误。

在ARC有效时,id型或对象型变量赋值给void*或者逆向赋值时都需要进行特定的转换。如果想单纯地赋值,可以使用 __bridige
转换。

id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;

但是转换为void*的__bridge转换,其安全性与赋值给__unsafe_unretained修饰符相近,甚至会更低。如果管理时不注意赋值对象的所有者,就会因悬垂指针而导致程序崩溃。

__bridge转换还有其他两种:__bridge_retained转换和__bridge_transfer转换

__bridge_retained转换可使要转换赋值的变量也持有所赋值的对象。__bridge_retained转换与retain相类似。

void *p = 0;
{
    id obj = [[NSObject alloc] init];
    p = (__bridge_retained void *)obj;
}
NSLog(@"class = %@",[(__bridge id)p class]);

运行结果为: 2017-12-06 16:39:06.148058+0800 ImageOrientation[7685:2312365] class = NSObject

变量作用域结束时,虽然随着持有强引用的变量obj失效,对象随之释放,但由于__bridge_retained转换使变量p看上去持有该对象的状态,因此该对象不会被废弃。

ARC无效时实现以上逻辑的代码为:

void *p = 0;
{
    id obj = [[NSObject alloc] init];
    /** [obj retainCount] ->1 */
    p = [obj retain];
    /** [obj retainCount] ->2 */
    [obj release];
    /** [obj retainCount] ->1 */
}
/*
 * [(id)p retainCount] ->1
 * 即[obj retainCount] ->1
 * 对象仍存在
 */

__bridge_transfer转换,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随之释放。__bridge_transfer转换与release相类似。

id obj = (__bridge_transfer id)p;
相当于ARC无效时代码如下:

/**ARC无效*/
id obj = (id)p;
[obj retain];
[p release];

__bridge转换,__bridge_retained转换,__bridge_transfer转换常用于OC对象和CoreFoundation对象之间的相互变换中。

  1. __bridge:CF和OC对象转化时只涉及对象类型不涉及对象所有权的转化;
  2. __bridge_transfer:常用在讲CF对象转换成OC对象时,将CF对象的所有权交给OC对象,此时ARC就能自动管理该内存;(作用同CFBridgingRelease())
  3. __bridge_retained:(与__bridge_transfer相反)常用在将OC对象转换成CF对象时,将OC对象的所有权交给CF对象来管理;(作用同CFBridgingRetain())

以下函数可用于OC对象和CoreFoundation对象之间的相互变换,成为Toll-Free Bridge “免费桥”转换。

CFTypeRef CFBridgingRetain(id X){
    return (__bridge_retained CFTypeRef)X;
}

id CFBridgingRelease(CFTypeRef X){
    return (__bridge_transfer id)X;
}

下例为将生成并持有的NSMutableArray对象作为CoreFoundation对象来处理。

CFMutableArrayRef cfObject = NULL;
{
    id obj = [[NSMutableArray alloc] init
    /**变量obj持有对生成并持有对象的强引用 */
    
    cfObject = CFBridgingRetain(obj);
    /** 通过CFBridgingRetain将对象CFRetain赋值给变量cfObject */
    
    CFShow(cfObject);
    printf("retain count =%dn",CFGetRetainCount(cfObject));
    /** 通过变量obj的强引用和通过CFBridgingRetain,引用计数为2
}
/** 因为变量obj超出作用范围,所以其强引用失效,引用计数为1 */
printf("retain count after the scope =%dn",CFGetRetainCount(cfObject));

CFRelease(cfObject);
/** 因为将对象CFRelease,所以其引用计数为0,古该对象被废弃。*/

运行结果为:

(
)
retain count =2
retain count after the scope =1

由此可知,Foundation框架的API生成并持有的OC对象能够作为CF对象来使用,也可以通过CFRelease来释放。以上代码也可以用__bridge_retained转换来代替。

CFMutableArrayRef cfObject = (__bridge_retained CFMutableArrayRef) obj;

如果使用__bridge转换,第一句打印的结果是1,因为__bridge转换不改变对象的持有状况,obj持有NSMutableArray对象的强引用,所以为1.后一句打印crash,因为obj超出其作用域,所以强引用失效,对象释放,无持有者的对象被废弃,所以出现悬垂指针,导致崩溃。

那么,将CoreFoundation的API生成并持有对象,将该对象作为NSMutableArray对象来处理,代码如下:

{
    CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    printf("retain count =%dn",CFGetRetainCount(cfObject));
    /*
     * CoreFoundation框架的API生成并持有对象
     * 之后的对象引用计数为1
     */
    id obj = CFBridgingRelease(cfObject);
    /*
     * 通过CFBridgingRelease赋值,变量obj持有对象强引用的同时,对象通过CFRelease释放
     */
    
    printf("retain count after the cast =%dn",CFGetRetainCount(cfObject));
    /*
     * 因为只有变量obj持有对生成并持有对象的强引用,所以引用计数为1
     * 另外,经由CFBridgingRelease转换后,赋值给变量cfObject中的指针也指向仍在存在的对象,所以可以正常使用。
     */
    NSLog(@"class =%@",obj);
    
}
/*
 * 因为变量obj超出作用域,所以其强引用失效,对象得到释放,无所有者的对象随之被废弃。
 */

运行结果如下:

retain count =1
retain count after the cast =1
2017-12-06 21:30:02.473804+0800 ImageOrientation[8249:2573155] class =(
)

也可以使用__bridge_transfer转换代替CFBridgingRelease。

id obj = (__bridge_transfer id)cfObject;

如果使用__bridge来替代__bridge_transfer或CFBridgingRelease转换,则第一句打印为1,中间打印为2,因为obj和cfObject同时持有对象的强引用,所以为2。超出范围后obj强引用失效,对象的引用计数为1。

ARC属性

ARC有效时,Objective-C类的属性也会发生变化。具体如下所以:

属性声明的属性 所有权修饰符
assign __unsafe_unretained修饰符
copy __strong修饰符(但是赋值的是被复制的对象)
ratai __strong修饰符
unsafe_unretained __unsafe_unretained修饰符
weak __weak修饰符

以上各属性复制给指定的属性中就想弹鼓赋值给附加各属性对应的所有权修饰符的变量中。只有copy属性不是简单的赋值,他赋值的是通过NSCopying接口的copyWithZone:方法复制复制源所生成的对象。

稀土掘金稿源:稀土掘金 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 移动开发 » Objective-C 内存管理之ARC规则

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录