自动引用计数ARC

Posted 沉江小鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自动引用计数ARC相关的知识,希望对你有一定的参考价值。

1.什么是自动引用计数?

顾明思义,自动引用计数(ARC,Automatic Reference Counting)是指内存管理中对引用采取自动计数的技术。

在OC中采用ARC机制,让编译器来进行内存管理。在新一代apple LLVM编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这在降低程序崩溃、内存泄露等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不在被使用的对象。

1.2 内存管理/引用计数

1.2.1 概要

OC中的内存管理,也就是引用计数。可以用开关房间的等为例来说明引用计数的机制,比如:上班进入办公室需要照明,下班离开办公室不需要照明。

假设办公室里的照明设备只有一个。上班进入办公室的人需要照明,所以要把灯打开,而对于下班离开办公室的人来说,已经不需要照明了,所以要把灯关掉。若是很多人上下班,每个人都开灯或是关灯,那么办公室的情况又将如何呢?最早下班离开的人如果关了灯,办公室里还没有走的人都将处于一片黑暗当中。

解决这个问题的办法是使办公室在还有至少1人的情况下保持开灯状态,而在无人时保持关灯状态。

a.第一个人进入办公室,“需要照明的人数”加1,计数值从0变成了1,因此要开灯。

b.之后每当有人进入办公室,"需要照明的人数"就加1,计数值从1变成2.

c.每当有人下班离开办公室,"需要照明的人数"就减1。如计数值从2变成1.

d.最后一个人下班离开办公室时,"需要照明的人数"减1,计数值从1变成了0,因此要关灯。

对办公室照明设备所做的动作和对OC对象所做的动作的对比。

对照明设备所做的动作:开灯 - 需要照明 - 不需要照明 - 关灯

对OC对象所做的动作:  生成对象 - 持有对象 - 释放对象 - 废弃对象。

使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理。同样,使用引用计数功能,对象也就能够得到很好的管理,这就是OC的内存管理。

1.2.2 内存管理的思考方式

自己生成的对象,自己所持有。

非自己生成的对象,自己也能持有。

不在需要自己持有的对象时释放。

非自己持有的对象无法释放。

//  对应的方法

对象操作:生成并持有对象 - 持有对象 - 释放对象 - 废弃对象

OC方法  :alloc/new/copy/mutablecopy - retain - release - dealloc

这些有关OC内存管理的方法,实际上不包括在该语言中,而是包含在cocoa框架中,用于ios开发。cocoa框架中Foundation框架类库的NSObject类担负内存管理的职责。OC内存管理中的alloc/retain/release/dealloc方法分别指代NSObject类的alloc类方法、retain实例方法、release实例方法和dealloc实例方法。

自己生成对象,自己所持有

使用alloc new copy mutableCopy 开头的方法名意味着自己生成的对象只有自己持有。本文所说的“自己”固然对应前文提到的“对象的使用环境”,但将之理解为编程人员“自身”也是没错的。下面写出了自己生成并持有对象的代码,为生成并持有对象,我们使用alloc 方法。

//    自己生成并持有对象
id obj = [[NSObject alloc] init];

//   使用NSObject类的alloc类方法就能自己生成并持有对象。指向生成并持有对象的指针被赋给变量obj,另外,使用如下new类方法也能生成并持有对象。
//  自己生成并持有对象
id obj = [NSObject new];

copy 方法利用基于NSCopying方法约定,由各类实现的copyWithZone,方法生成并持有对象的副本。与copy方法类似,mutableCopy方法利用基于NSMutableCopying方法约定,由各类实现的mutableCopyWithZone方法生成并持有对象的副本。两者的区别在于,copy方法生成不可变更的对象,而mutableCopy方法生成可变更的对象。这类似于NSArray和NSMutableArray的差异。

allocMyObject newThatObject copyThis mutableCopyYourObject 方法也意味着自己生成并持有对象。

但是allocate newer copying mutableCopyed 并不属于同一类别的方法。

非自己生成的对象,自己也能持有 

用上述项目之外的方法取得的对象,即用alloc/new/copy/mutableCopy 以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。我们来使用NSMutableArray类的array类方法。

//    取得非自己生成并持有的对象
id obj = [NSMutableArray array];

//    取得的对象存在,但是自己不持有对象,源代码中,NSMutableArray类对象被赋值给obj,但变量obj自己并不持有该对象,使用retain方法可以持有对象。

//    取得非自己生成并持有的对象,取得的对象虽在,但是自己并不持有
id obj = [NSMutableArray array];

//    自己持有对象,通过retain方法,非自己生成的对象跟用alloc/new/copy/mutableCopy方法生成并持有的对象一样,成为了自己所持有的。
[obj retain]

不需要自己持有的对象时释放

自己持有的对象,一旦不再需要,持有者有义务释放该对象,释放使用release方法。 

//    自己生成并持有对象
id obj = [[NSObject alloc] init];

//    释放对象,指向对象的指针仍然被保留在变量obj中,貌似能够访问。但是对象一经释放绝对不可以访问。

如此,用alloc方法由自己生成并持有的对象就通过release方法释放了,自己生成而非自己所持有的对象,若用retain方法变为自己持有,也同样可以用release方法释放。

//    取得非自己生成并持有的对象
id obj = [NSMutableArray array]

//    自己持有对象
[obj retain]

//    自己释放对象,对象不可再被访问
[obj release]

 如果要用某个方法生成对象,并将其返还给该方法的调用方,那么他的源代码又是什么样的呢?

//    如下代码,原封不动地返回用alloc方法生成并持有的对象,就能让调用方也持有该对象。allocObject 名称符合命名规则,因此它与用alloc方法生成并持有对象的情况完全相同,所以使用allocObject方法也就意味着“自己生成并持有对象”

- (id)allocObject{
   //    自己生成并持有对象
   id obj = [[NSObject alloc] init];
   //     自己持有对象
   return obj;
}

//  调用[NSMutableArray array] 方法使取得的对象存在,但自己不持有对象,又是如何实现的?根据上文的命名规则,不能使用以alloc/new/copy/mutableCopy开头的方法名,因此要使用object这个方法名。

- (id)object{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    return obj;  
}

//    上例中,我们使用了autorelease方法,用该方法,可以使取得的对象存在,但自己不持有对象。

 autorelease 提供这样的功能,使对象在超出指定的生存范围时能够自动并正确的释放(调用release方法)下面是release和autorelease的区别

无法释放非自己持有的对象

对于非alloc/new/copy/mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此之外所得到的对象绝对不能释放。倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。例如自己生成并持有对象后,在释放完不再徐亚ode对象之后再次释放。

//    自己生成并持有对象
id obj = [[NSObject alloc] init];

//    对象已释放
[obj release];

//    释放之后再次释放已非自己持有的对象,应用程序崩溃,崩溃情况,再度废弃已经废弃了的对象时崩溃,访问已经废弃的对象时崩溃。
----------------------------------------------------------------------------------------------------------

//    取得的对象存在,但自己不持有对象
id objq = [obj0 object]

//    释放了非自己持有的对象,这肯定会导致应用程序崩溃

 如这些例子所示,释放非自己持有的对象会造成程序崩溃。因此绝对不要去释放非自己持有的对象。

1.2.3 alloc/retain/release/dealloc 实现

包含NSObject类的Foundation框架并没有公开,不过,Foundation框架使用的Core Foundation框架的源代码,以及通过调用NSObject类进行内存管理部分的源代码是公开的。但是,没有NSObject类的源代码,就很难了解NSObject类的内部实现细节,所以,我们首先使用开源软件GNUstep来说明。

GNUstep是Cocoa框架的互换框架,也就是说,GUNstep的源代码虽不能说和Cocoa实现完全相同,但是从使用者角度来看,两者的行为和实现方式是一样的,或者说非常相似,

id obj = [NSObject alloc];

//    这里调用了NSObject类的alloc类方法在NSObject.m源代码中的实现如下。

GNUstep/modules/core/base/Source/NSObject.m alloc

+(id)alloc{
    return [self allocWithZone: NSDefaultMallocZone( )];
}

+(id)allocWithZone: (NSZone *)z{
    return NSAllocateObject (self, 0 , z);
}

//    通过allocWithZone类方法调用NSAllocateObject函数分配了对象,下面我们看看NSAllocateObject函数。

GNUstep/modules/core/base/Source/NSObject.m  NSAllocateObject

struct obj_layout{
    NSUInteger retained;
}

inline id
NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone){
    int size = 计算容纳对象所需内存大小
    id new = NSZoneMalloc(zone,size);
    memset(new, 0, size);
    new = (id)& ((struct obj_layout *)new)[1];  
}

//    NSAllocateObject 函数通过调用NSZoneMalloc函数来分配存放对象所需的内存空间,之后将该内存空间置为0,最后返回作为对象而使用的指针。

 注:NSDefaultMallocZone,NSZoneMalloc等名称中包含的NSZone是什么呢?它是为了防止内存碎片化而引入的结构,对内存分配的区域本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。

但是,如同苹果官方文档中所说,现在的运行时系统只是简单地忽略的区域的概念,运行时系统中的内存管理本身已经极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。

下面是去掉NSZone后简化了的源代码:

GNUstep/modules/core/base/Source/NSObject.m alloc 简化版

struct obj_layout {
    NSUInteger retained;
}

+ (id) alloc{
    int size = sizeof (struct obj_layout) + 对象大小
    struct obj_layout * p = (struct obj_layout *) calloc (1,size);
    return (id) (p+1)
}

 alloc类方法用struct obj_layout中的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0后返回。

对象的引用计数可通过retainCount实例方法取得

id obj = [[NSObject alloc] init];
NSLog(@"retainCount = %d",[objc retainCount]);

//    显示retainCount = 1
执行alloc后对象的retainCount 是 “1”,下面通过GNUstep的源代码来确认。

- (NSUInteger) retainCount{
   return NSExtraRefCount (self) + 1;
}

inline NSUInteger
NSExtraRefCount (id anObject){
    return ((struct obj_layout *) anObject) [-1].retained;
}

1.2.4 苹果的实现

在NSObject 类的alloc类方法上设置断点,追踪程序的执行,以下列出了执行所调用的方法和函数。

+alloc

+allocWithZone

class_createInstance

calloc

alloc类方法首先调用allocWithZone类方法,这和GNUstep的实现相同,然后调用class_createInstance函数,最后通过调用calloc来分配内存块。这和前面的GNUstep的实现并无多大差异。

retainCount/retain/release实例方法又是怎么实现的呢?下面列出各个方法分别调用的方法阿和函数

-retainCount

_CFDoExternRefOperation

CFBasicHashGetCountOfKey

 

-retain

_CFDoexternRefOperation

CFBasicHashAddValue

 

-release 

_CFDoExternRefOperation

CFBasicHashRemoveValue

(CFBasicHashRemoveValue 返回0 时,-release调用dealloc)

各个方法都通过同一个调用了_CFDoExternRefOperation函数,调用了一系列名称相似的函数。如这些函数名的前缀"CF"所示,它们包含于Core Foundation框架源代码中,即是CFRuntime.c的_CFDoExternRefOperation函数。

1.2.5 autorelease

autorelease 就是自动释放,这看上去很像ARC,但实际上它更类似于C语言中自动变量(局部变量)的特性。

在C语言中,程序执行时,如果某自动变量超出其作用域,该自动变量将被自动废弃。

{
   int a;
}

//    因为超出变量的作用域,自动变量 int a将被废弃,不可再访问。

 

autorelease会像C语言的自动变量那样来对待对象实例。当超出其作用域(相当于变量作用域)时,对象实例的release实例方法被调用。另外,同C语言的自动变量不同的是,编程人员可以设定变量的作用域。

autorelease的具体使用方法如下:

(1)生成并持有NSAutoreleasePool 对象。

(2)调用已分配对象的autorelease实例方法

(3)废弃NSAutoreleasePool对象。

NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。

源代码如下:

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

//    这里的[pool drain] 等同于 “[obj release]”

 在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其它程序可运行的地方,对NSAutoreleasePool对象进行生成,持有和废弃处理。因此,应用程序开发者不一定非得使用该对象进行开发工作。

尽管如此,但在大量产生autorelease的对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。典型的例子就是读取大量图像的同时改变其尺寸。图像文件读入到NSData对象,并从中生成UIImage对象,改变该对象尺寸后生成新的UIImage对象,这种情况下,就会大量产生autorelease的对象。

在此情况下有必要在适当的地方,生成,持有或废弃Pool对象。

通常在使用OC,也就是Foundation框架时,无论调用哪一个对象的autorelease实例方法,实现上是调用的都是NSObject类的autorelease实例方法。但是对于NSAutoreleasePool类,atorelease实例方法已被该类重载,因此运行时就会出错。

1.3 ARC规则

1.3.1 概要

实际上“引用计数式”内存管理的本质部分在ARC中并没有改变,就像 “自动引用计数”这个名称表示的那样,ARC只是自动的帮助我们处理“引用计数”的相关部分。

对某个文件可选择使用或不使用ARC.

1.3.2 内存管理的思考方式

自己生成的对象,自己持有

非自己生成的对象,自己也能持有

自己持有的对象不再需要时释放

非自己持有的对象无法释放

1.3.3 所有权修饰符

OC编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。

所谓对象类型就是指向NSObject这样的OC类的指针,如 “NSObject *”。id类型用于隐藏对象类型的类名部分,相当于C语言中常用的 “void *”

ARC有效时,id类型和对象类型通C语言其它类型不同,其类型上必须附加所有权修饰符。

__strong __weak __unsafe_unretained __autoreleasing

__strong 修饰符

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

id obj = [[NSObject alloc] init];

id 和对象类型在没有明确指定所有权修饰符时,默认为__strong修饰符。上面的代码与以下相同。

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

该源代码再ARC无效的时候又该如何表述呢?

//    ARC无效
id obj = [[NSObject alloc] init];

//    该源代码一看则名,目前在表面上没有任何变化,在看看下面的代码
{
    id __strong obj = [[NSObject alloc] init];
}

此源代码明确指定了C语言的变量的作用于。ARC无效的时候,该源代码可记述如下:
//   ARC无效
{
    id obj = [[NSObject alloc] init];
    [obj release];
}

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

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

如 "strong" 这个名称所示,__strong 修饰符表示对对象的"强引用"。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

下面关注一下源代码中关于对象的所有者的部分

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

//    此源代码就是之前自己生成并持有对象的源代码,该对象的所有者如下:

{
    //    自己生成并持有对象
   id __strong obj = [[NSObject alloc] init]
   
    //    因为变量obj为强引用,所以自己持有该对象
}

//    因为变量obj超出其作用域,强引用失效,所以自动地释放自己持有的对象,对象的所有者不存在,因此废弃该对象。

 此处,对象的所有者和对象的生存周期是明确的,那么,在取得非自己生成并持有的对象时又会如何呢?

//    在NSMutableArray类的array类方法的源代码中取得非自己生成并持有的对象,具体如下

{  
     //    取得非自己生成并持有的对象    
     id __strong obj = [NSMutableArray array];
    
     //     因为变量obj为强引用,所以自己持有对象
}

//    因为变量obj超出其作用域,强引用失效,所以自动地释放自己持有的对象

在这里对象的所有者和对象的生存周期也是明确的

{
    //    自己生成并持有对象
    id __strong obj = [[NSObject alloc] init];
    //    因为变量obj为强引用,所以自己持有对象
   
}

 //    因为变量obj超出其作用域,强引用失效,所以自动地释放自己持有的对象,对象的所有者不存在,因此废弃该对象。

 当然,附有__strong 修饰符的变量之间可以相互赋值。

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;
此时持有对象B的强引用的变量为obj0,obj2

obj2 = nil;
此时持有对象B的强引用的变量为obj0

obj3 = nil;
此时持有对象B的强引用的变量没有,所以废弃对象B

 __weak 修饰符

两个对象循环引用的例子。

//    ARC下 对象的权限修饰符是__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;
}

//    以下为循环引用。
{
   // test0 持有对象a的强引用
   id test0 = [[Test alloc] init]; // 对象a
   // test1 持有对象b的强引用
   id test1 = [[Test alloc] init]; // 对象b
   // 对象a的obj成员变量持有对象b的强引用
   [test0 setObject:test1];
   //  对象b的obj成员变量持有对象a的强引用
   [test1 setObject:test0];
}

//    因为test0变量超出其作用域,强引用失效,所以释放对象a
//    因为test1变量超出其作用域,强引用失效,所以释放对象b
//    此时持有对象a的强引用的变量为对象b的obj
//    此时持有对象b的强引用的变量为对象a的obj
发生内存泄露

// 循环引用容易发生内存泄露,所谓内存泄露就是应当废弃的对象在超出其生存周期后继续存在。此代码的本意是赋予变量test0的对象a和对象b在超出其变量作用域时被释放,即在对象不被任何变量持有的状态下予以废弃。但是,循环引用使得对象不能被再次废弃。

像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)

id test = [[Test alloc] init];

[test setObject:test];

怎么样才能避免循环引用呢?看到__strong修饰符就会意识到了,既然有strong,就应该有与之对应的weak,也就是说,使用__weak修饰符可以避免循环引用。

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

id __weak obj = [[NSObject alloc] init];
变量附加上了 __weak 修饰符,实际上如果编译以下代码,编译器会发出警告

warning: assigning retained obj to weak variable; obj will be released after assignment [-Warc-unsafe-retained-assign]

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

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

 因为带__weak 修饰符的变量(即弱引用)不持有对象,所以在超出其变量作用域时,对象即被释放如果像下面这样将先前可能发生循环引用的类成员变量改成附有__weak修饰符的成员变量的话,该现象可以避免。

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

//    互相弱引用

对象a             ---->    对象b

id __weak obj    <----    id __weak obj

 __weak修饰符还有另一个优点,在持有某对象的弱引用的时候,若该对象被废弃,这个弱引用也会被置为nil

id __weak obj0 = nil;
{
   id obj1 = [[NSObject alloc] init]; //    对象a
   obj0 = obj1;
}

//    对象a出了作用域被释放掉,所以弱引用obj0也会被置为nil

 像这样,使用__weak可以避免循环引用。

 __unsafe_unretained 修饰符

__unsafe_unretained 修饰符正如其名unsafe所示,是不安全的所有权修饰符。尽管ARC式的内存管理是编译器的工作,但附有__unsafe_unretained修饰符的变量不属于编译器的内存管理的对象,这一点在使用的时候要注意。

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

//    该源代码将自己生成并持有的对象赋值给附有__unsafe_unretained修饰符的变量中。虽然使用了 unsafe 的变量,但是编译器不会忽略,而是给出适当的警告。

warning:assignin retained obj to unsafe_unretained variable;obj will be released after assignment

 附有 __unsafe_unretained 修饰符的变量同附有__weak 修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。到这里,__unsafe_unretained修饰符和__weak修饰符是一样的,下面我们看下源代码的差异:

id __unsafe_unretained obj1 = nil;
{
// 自己生成并持有对象,因为obj0变量为强引用,所以自己持有对象 id __strong obj0 = [[NSObject alloc] init];
  // 虽然obj0变量赋值给obj1,但是obj1变量既不持有对象的强引用,也不持有弱引用
obj1 = obj0; NSLog(@"A: %@",obj1); }
// 因为obj0变量超出其作用域,强引用失效,所以自动释放自己持有的对象。因为对象无持有者所以废弃该对象。
// 输出obj1 变量表示的对象 obj1变量表示的对象已经被废弃(悬垂指针) 错误访问 NSLog(@"B: %@",obj1); // 执行的结果为 A : <NSObject :0x753e180> B : <NSObject :0x753e180>

也就是说,最后一行的NSLog只是碰巧正常运行而已。虽然访问了已经被废弃的对象,但是应用程序在个别运行状况下才会崩溃。

再试用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在。

为什么需要使用附有__unsafe_unretained修饰符的变量?

比如在iOS4以及 OS X Snow Leopard 的应用程序中,必须使用 __unsafe_unretained修饰符来替代__weak修饰符,赋值给附有 __safe_unretained 修饰符变量的对象在通过该变量使用时,如果没有确保其确实存在,那么应用程序就会崩溃。 

__autoreleasing 修饰符

ARC有效时不能使用autorelease方法,另外也不能使用NSAutoreleasePool类。这样一来,虽然autorelease无法直接使用,但实际上ARC有效时autorelease功能是起作用的。

// ARC无效

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

id obj = [[NSObject alloc] init];

[obj autorelease];

[pool drain];

// ARC有效的时候,该源代码也能写成下面这样

@autoreleasepool{

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

}

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

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

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

但是,显示地附加__autoreleasing修饰符同显式的附加__strong修饰符一样罕见。

我们通过实例看看为什么非显示的使用__autoreleasing修饰符也可以。

取得非自己生成并持有的对象时,如同以下源代码,虽然可以使用alloc/new/copy/mutableCopy以外的方法来取得对象,但该对象已被注册到了autoreleasepool。这同在ARC无效时取得调用了autorelease方法的对象是一样的。这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool.init方法返回值的对象不会注册到autoreleasepool。

@autoreleasepool {

//  取得非自己生成并持有的对象

id __strong obj = [NSMutableArray array];

//  因为变量obj为强引用,所以自己持有对象,并且该对象由编译器判断其方法名后自动注册到autoreleasepool       

}

// 因为变量obj超出其作用域,强引用失效,所以自动释放自己持有的对象。
同时,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放,
因为对象的所有者不存在,所以废弃对象

 像这样,不适用__autoreleasing修饰符也能使对象注册到autoreleasepool.以下为取得非自己生成并持有对象时被调用方法的源代码实例。

+ (id) array{
    return [[NSMutableArray alloc] init];
}

//    该代码没有使用__autoreleaseing修饰符,可写成以下形式

+ (id) array{
    id obj = [[NSMutableArray alloc] init];
    return obj;
}

 因为没有显示指定所有权修饰符,所以id obj同附有__strong 修饰符的 id __strong obj是完全一样的。由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器会自动将其注册到autoreleasepool。

以下为使用__weak修饰符的例子。虽然__weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool的对象。

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

// 以下源代码与此相同

id __weak obj1 = obj0;

id __autoreleasing tmp = obj1;

NSL(@"class = %@", [tmp class]);

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

最后一个可非显示的使用 __autoreleasing修饰符的例子,同前面讲述的 id obj 和 id __strong obj 完全一样吗。那么id的指针 id * obj 又如何呢?可以由 id __strong obj 的例子类推出 id __strong *obj 吗? 其实,推出来的是 id __autoreleasing *obj。同样的,对象的指针 NSObject **obj 便成为了 NSObject *__autoreleasing * obj;

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

比如,为了得到详细的错误信息,经常会在方法的参数中传递NSError对象的指针,而不是函数返回值。Cocoa框架中,大多数方法也使用这种方式,如NSString 的 stringWithContentsOfFile:encoding:error类方法等。使用该方式的源代码如下所示:

NSError *error = nil;

BOOL result = [obj performOperationWithError:&error];

该方法的声明为:

- (BOOL) performOperationWithError:(NSError **)error;

同前面讲述的一样,id的指针或对象的指针会默认附加上__autoreleasing 修饰符,所以等同于以下源代码

- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

 参数中持有NSError对象指针的方法,虽然为响应其执行结果,需要生成NSError类对象,但也必须符合内存管理的思考方式。

作为alloc/new/copy/mutableCopy方法返回值取得的对象是自己生成并持有的,其它情况下便是取得非自己生成并持有的对象。因此,使用附有__autoreleasing修饰符的变量作为对象取得参数,与除alloc/new/copy/mutableCopy外其他方法的返回值取得对象完全一样,都会注册到autoreleasepool,并取得自己生成并持有的对象。

比如performOperationWithError 方法的源代码就应该是下面这样

- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error{
  
 // 错误发生
 *error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
return NO;

}

// 因为声明为NSError *__autoreleasing *类型的error作为*error被赋值,所以能够返回注册到autoreleasepool中的对象

 然而,下面的源代码会产生编译器错误:

NSError *error = nil;

NSError **pError = &error;

赋值给对象指针时,所有权修饰符必须一致。

error:initializing \'NSError *__autoreleasing *\' with an expression of type \'NSError *_strong *\'changes retain/release properties of pointer 

此时,对象指针必须加__strong 修饰符

NSError *error = nil;
NSError *__strong *pError = &error;
// 编译正常

当然,对于其他所有权修饰符也是一样

NSError __weak *error = nil;
NSError * __weak *pError = &error;
// 编译正常

NSError __unsafe_unretained *unsafeError = nil;
NSError * __unsafe_unretained *pUnsafeError = &unsafeError;
// 编译正常

 前面的方法参数中使用了附有__autoreleasing 修饰符的对象指针类型。

- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error;

 然而调用方却使用了附有__strong 修饰符的对象指针类型;

NSError __strong *error = nil;

BOOL result = [obj  performOperationWithError:&error];

 对象指针型赋值时,其所有权修饰符必须一致,但为什么该源代码没有警告就顺利通过编译了呢?实际上,编译器自动地将该源代码转化成了下面的形式。

NSError __strong *error = nil;

NSError __autoreleasing *tmp = error;

BOOL result = [obj performOperationWithError:&tmp];

error = tmp;

 当然也可以显示地指定方法参数中对象指针类型的所有权修饰符

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

 像该源代码的声明一样,对象不注册到autoreleasepool也能够传递。但是前面也说过,只有作为alloc/new/copy/mutableCopy 方法的返回值而取得对象时,能够自己生成并持有对象。其他情况即为“取得非自己生成并持有的对象”,这些务必牢记。为了在使用参数取得对象时,贯彻内存管理的思考方式,我们要将参数声明为附有__autoreleasing修饰符,但在显示的指定 __autoreleasing 修饰符时,必须注意对象变量要为自动变量(包括局部变量、函数以及方法参数)。

 

下面我们换个话题,详细了解一下@autoreleasepool.如以下源代码所示,ARC无效时,可以将NSAutoreleasePool对象嵌套使用。

// ARC无效
NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init];

NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];

NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];

id obj = [[NSObject alloc] init];

[obj autorelease];

[pool2 drain];

[pool1 drain];

[pool0 drain];

同样的,@autoreleasepool 也能够嵌套使用

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

 比如,在iOS应用程序模板中,像下面的main函数一样,@autoreleasepool块包含了全部程序。

int main(int argc, char *argv[]){
  @autoreleasepool{
   return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

 NSRunLoop等实现不论ARC有效还是无效,均能够随时释放注册到autoreleasepool中的对象。

另外,如果编译器版本为LLVM3.0以上,即使ARC无效@autoreleasepool块也能够使用,如下:

// ARC无效
@autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}

 因为autoreleasepool 范围以块级源代码表示,提高了程序的可读性,所以今后在ARC无效时也推荐使用 @autoreleasepool块。

另外,无论ARC是否有效,调试用的非公开函数_objc_autoreleasePoolPrint()都可以使用

_objc_autoreleasePoolPrint()

利用这一函数可有效的帮助我们调试注册到autoreleasepool 上的对象。

1.3.4 规则

- 不能使用 retain/realese/retainCOunt/autorelease

- 不能使用 NSAllocateObject/NSDeallcateObject

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

- 不要显示调用dealloc

- 使用@autoreleasepool代替 NSAutoreleasePool

- 不能使用区域 (NSZone)

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

- 显示转化 id  和 void *

在ARC无效时,像一下代码这样将id变量强制转换成 void * 变量并不会出问题。

// ARC无效
id obj = [[NSObject alloc] init];

void *p = obj;

更近异步,将该void * 变量赋值给 id 变量中,调用其实例方法,运行时也不会有问题。

// ARC无效

id o = p;

[o release];

// 但是在ARC有效时这便会引起编译错误

error : implicit coversion of an OC pointer to \'void *\' is disallowed with ARC

 id 型或对象型变量赋值给 void * 或者逆向赋值时都需要进行特定的转换。如果只想单纯的赋值,则可以使用 "__bridge 转换"

id obj = [[NSObject alloc] init];

void *p = (__bridge void *)obj;

id o = (__bridge id)p

 像这样,通过 “__bridge转换”, id 和 void * 就能够相互转换。

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

__bridge 转换中还有另外两种转换,分别是 “__bridge_retained 转换” 和 “__bridge_transfer” 转换。

id obj = [[NSObject alloc] init];

void *p = (__bridge_retained void *)obj

 __bridge_retained 转换可使要转换赋值的变量也持有所赋值的对象。 。。。。。

1.3.5 属性

当ARC有效时,oc类的属性也会发生变化

@property (strong ,nonatomic) NSString *name;

 当ARC有效时,以下可作为这种属性声明中使用的属性来用。

属性声明和属性和所有权修饰符的对应关系
属性声明的属性 所有权修饰符
assign __unsafe_unretained修饰符
copy   __strong 修饰符(但是赋值的是被赋值的对象)
retain __strong 修饰符
strong __strong 修饰符
unsafe_unretained __unsafe_unretained 修饰符
week __weak 修饰符

以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中,只有copy属性不是简单的赋值,它赋值的是通过NSCopying 结构的 copyWithZone 方法复制赋值源所生成的对象。

另外,在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误。比如下面的这种情况,

id obj

在声明 id obj 成员变量时,像下面这样,定义其属性为weak

编译器会报错。

此时,成员变量的声明中需要附加 __weak 修饰符。

id __weak obj;

或者使用 strong 属性替代 weak 属性

@property (nonatomic, strong ) id obj

 

 

以上是关于自动引用计数ARC的主要内容,如果未能解决你的问题,请参考以下文章

自动引用计数 (ARC) 中的 NSThreads

关于自动引用计数,我需要了解啥?

[OC学习笔记]自动引用计数

自动引用计数

[OC学习笔记]自动引用计数初学

Swift学习笔记-自动引用计数弱引用和无主引用