顾明思义,自动引用计数(ARC,Automatic Reference Counting)是指内存管理中对引用采取自动计数的技术。
在OC中采用ARC机制,让编译器来进行内存管理。在新一代apple LLVM编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这在降低程序崩溃、内存泄露等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不在被使用的对象。
OC中的内存管理,也就是引用计数。可以用开关房间的等为例来说明引用计数的机制,比如:上班进入办公室需要照明,下班离开办公室不需要照明。
假设办公室里的照明设备只有一个。上班进入办公室的人需要照明,所以要把灯打开,而对于下班离开办公室的人来说,已经不需要照明了,所以要把灯关掉。若是很多人上下班,每个人都开灯或是关灯,那么办公室的情况又将如何呢?最早下班离开的人如果关了灯,办公室里还没有走的人都将处于一片黑暗当中。
解决这个问题的办法是使办公室在还有至少1人的情况下保持开灯状态,而在无人时保持关灯状态。
a.第一个人进入办公室,“需要照明的人数”加1,计数值从0变成了1,因此要开灯。
b.之后每当有人进入办公室,"需要照明的人数"就加1,计数值从1变成2.
c.每当有人下班离开办公室,"需要照明的人数"就减1。如计数值从2变成1.
d.最后一个人下班离开办公室时,"需要照明的人数"减1,计数值从1变成了0,因此要关灯。
对办公室照明设备所做的动作和对OC对象所做的动作的对比。
对照明设备所做的动作:开灯 - 需要照明 - 不需要照明 - 关灯
对OC对象所做的动作: 生成对象 - 持有对象 - 释放对象 - 废弃对象。
使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理。同样,使用引用计数功能,对象也就能够得到很好的管理,这就是OC的内存管理。
自己生成的对象,自己所持有。
非自己生成的对象,自己也能持有。
不在需要自己持有的对象时释放。
非自己持有的对象无法释放。
// 对应的方法
对象操作:生成并持有对象 - 持有对象 - 释放对象 - 废弃对象
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 方法。
class="brush:objc;gutter:true;">// 自己生成并持有对象 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] // 释放了非自己持有的对象,这肯定会导致应用程序崩溃
如这些例子所示,释放非自己持有的对象会造成程序崩溃。因此绝对不要去释放非自己持有的对象。
包含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; }
在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函数。
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实例方法已被该类重载,因此运行时就会出错。
实际上“引用计数式”内存管理的本质部分在ARC中并没有改变,就像 “自动引用计数”这个名称表示的那样,ARC只是自动的帮助我们处理“引用计数”的相关部分。
对某个文件可选择使用或不使用ARC.
自己生成的对象,自己持有
非自己生成的对象,自己也能持有
自己持有的对象不再需要时释放
非自己持有的对象无法释放
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可以避免循环引用。