一、什么是自动引用计数
自动引用计数(ARC,Automic Reference Counting)是指内存管理中对引用采取自动计数的技术。
在Objective-C中采用Automic Reference Counting(ARC)机制,让编译器来进行内存管理。新一代编译器中设置ARC为有效状态,就无需再次键入retain
或者release
代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能够立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。
二、内存管理/引用计数
(一)内存管理的思考方式
思考方式:
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
除了“生成”、“持有”、“释放”三个词,还有一个“废弃”。
对象操作 | Objective-C方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
持有对象 | retain方法 |
释放对象 | release方法 |
废弃对象 | dealloc方法 |
这些内存管理的方法,实际上不包括在该语言中,而是包含在Cocoa框架中。Cocoa框架中的Foundation框架类库的NSObject类担负内存管理的职责。
1. 自己生成的对象,自己持有
使用以下名称开头的方法名意味着自己生成的对象只有自己持有:
- alloc
- new
- copy
- mutableCopy
//自己生成并持有对象
id obj = [[NSObject alloc] init];
//自己持有对象
//也可以写成:
//自己生成并持有对象
id obj2 = [NSObject new];
//自己持有对象
使用NSObject
类的alloc
类方法就能自己生成并持有对象。指向生成并持有对象的指针被赋给变量obj
。
copy
方法利用基于NSCopying
方法约定,由各类实现的copyWithZone:
方法生成并持有对象的副本。mutableCopy
与copy
方法类似。用这些方法生成的对象,虽然是对象的副本,但同alloc
、new
方法一样,在“自己生成并持有对象”。
2. 非自己生成的对象,自己也能持有
用上述项目之外的方法,即alloc/new/copy/mutableCopy以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。
//取得非自己生成并持有的对象
id obj = [NSMutableArray array];
//取得的对象存在,但自己不持有对象
源代码中,NSMutableArray类对象被赋给变量obj
,但变量obj
自己并不持有该对象。使用retain
方法可以持有对象。
//取得非自己生成并持有的对象
id obj = [NSMutableArray array];
//取得的对象存在,但自己不持有对象
[obj retain];
//自己持有对象
通过retain
方法,非自己生成的对象跟用alloc/new/copy/mutableCopy
方法生成并持有的对象一样,成为了自己持有的。
3. 不再需要自己持有的对象时释放
自己持有的对象,一旦不再需要,持有者有义务释放该对象。释放用release
方法。
//自己生成并持有对象
id obj = [[NSObject alloc] init];
//自己持有对象
[obj release];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似能够访问,
//但对象一经释放绝对不可访问
如此,用alloc
方法由自己生成并持有的对象就通过release
方法释放了。自己生成而非自己所持有的对象,若用retain
方法变为自己持有,也同样可以用release
方法释放。
id obj = [NSMutableArray array];
//取得的对象存在,但自己不持有对象
[obj retain];
//自己持有对象
[obj release];
//释放对象
//对象不可再被访问
用alloc/new/copy/mutableCopy
方法生成并持有的对象,或者用retain
方法持有的对象,一旦不再需要,务必要用release
方法进行释放。
如果使用某个方法生成对象,并将其返还给该方法的调用方,那么它的源代码又是怎样的呢?
- (id)allocObject {
//自己生成并持有对象
id obj = [[NSObject alloc] init];
//自己持有对象
return obj;
}
上例所示,原封不动地返回用alloc
方法生成并持有的对象,就能让调用方也持有该对象。
//取得非自己生成并持有的对象
id obj1 = [obj0 allocObject];
//自己持有对象
那么,调用[NSMutableArray array]
方法使取得的对象存在,但自己不持有对象,又是如何实现的呢?
- (id)object {
id obj = [[NSOBject alloc] init];
//自己持有对象
[obj autoRelease];
//取得的对象存在,但自己不持有对象
return obj;
}
上例中,我们使用了autoRelease
方法。用该方法,可以使取得的对象存在,但自己不持有对象。autoRelease
方法提供这样的功能,使对象在超出指定的生存范围时能够自动并正确的释放(调用release
方法)。
使用NSMutableArray
类的array
方法等可以取得谁都不持有的对象,这些方法都是通过autorelease
实现的。
当然,也能够通过retain
方法将调用autorelease
方法取得的对象变为自己持有。
id obj1 = [obj0 object];
//取得的对象存在,但自己不持有对象
[obj1 retain];
//自己持有对象
4. 无法释放非自己持有的对象
对于用alloc/new/copy/mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放,而由此之外所得到的对象绝对不能释放。倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。
//自己生成并持有对象
id obj = [[NSObject alloc] init];
//自己持有对象
[obj release];
//对象已释放
[obj release];
//释放后再次释放已非自己持有的对象
//应用程序崩溃
//崩溃情况:
//再度废弃已经废弃了的对象时崩溃
//访问已经废弃的对象时崩溃
id obj1 = [obj0 object];
//取得的对象存在,但自己不持有对象
[obj1 release];
//释放了非自己持有的对象
//这肯定会导致应用程序崩溃
(二)alloc/retain/release/dealloc实现
接下来,以OC内存管理中使用的alloc/retain/release/dealloc
方法为基础,通过实际操作来理解内存管理。
没有NSObject
类的源代码,很难了解NSObject
类的内部实现细节,为此,我们首先使用开源软件GNUstep说明。GNUstep是Cocoa框架的互换框架。也就是说,它的源代码虽不能说和苹果的Cocoa实现完全相同,但是从使用者的角度看,两者的行为和实现方式是一样的,或者说非常相似。理解了GNUstep源代码也就相当于理解了苹果的Cocoa实现。
下面来看看GNUstep源代码中NSObject
类的alloc
类方法。为了明确重点,有的地方对引用的源代码进行了摘录或者修改。
id obj = [NSObject alloc];
上述调用NSObject
类的alloc
类方法在NSObject.m
的源代码中的实现如下:
+ (id) alloc {
return [self allocWithZone:NSDefaultMallocZone()];
}
+ (id) allocWithZone:(NSZone*)z {
return NSAllocateObject(self, 0, z);
}
通过allocWithZone:
类方法调用NSAllocateObject
函数分配了对象。下面来看看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_lyout *)new)[1];
}
NSAllocateObject
函数通过调用NSZoneMalloc
函数来分配存放对象所需的内存空间,之后将该内存空间置0,最后返回作为对象而使用的指针。
NSZone
是为了防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。但是,如同苹果官方文档说的那样,现在运行时系统只是简单地忽略了区域的概念。运行时系统中的内存管理本身已极具效率。使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。
以下是去掉NSZone后简化的源代码:
struct objz_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", [obj 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;
}
由对象寻址找到对象内存头部,从而访问其中的retained
变量。
因为分配时全部置0,所以retained
为0。由NSExtraRefCount(self) + 1
得出,retainCount
为1。可以推断出,retain
方法使retained
变量加1,而release
方法使retained
变量减1。
[obj retain];
下面来看一下上面那样调用出的retain
实例方法。
- (id)retain {
NSIncrementExtraRefCount(self);
return self;
}
inline void
NSIncrementExtraRefCount(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1)
[NSException raise:NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"];
((struct obj_layout *)anObject)[-1].retained++;
}
虽然写入了当retained
变量超出最大值时发生异常的代码,但实际上只运行了使retained
变量加1的retained++
代码。同样的,release
实例方法进行retained--
并在该引用计数变量为0时做出处理。下面看源码:
[obj release];
以下为此release
方法的实现:
- (void)release {
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL
NSDecrementExtraRefCountWasZero(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
return NO;
}
}
当retained变量大于0时减1,等于0时调用dealloc实例方法,废弃对象。以下是废弃方法时所调用的dealloc实例方法的实现。
- (void)dealloc {
NSDeallocateObject(self);
}
inline void
NSDeallocateObject (id anObject) {
struct obj_layout *o = &((struct obj_layout*)anObject)[-1];
free(o);
}
上述代码仅废弃由alloc分配的内存块。
总结:
- 在OC的对象中存有引用计数这一整数值。
- 调用
alloc
或是reatin
方法后,引用计数值加1 - 调用
release
后,引用计数值减1. - 引用计数值为0时,调用dealloc方法废弃对象。
(三)苹果的实现
因为NSObject类的源码没有公开,此处利用Xcode的调试器和iOS大概追溯出其实现过程。
在NSObject类的alloc类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数:
+ alloc
+ allocWithZone:
class_createInstance
calloc
alloc 类方法首先调用allocWithZone: 类方法,这和GNUstep 的实现相同,然后调用class_createInstance 函数,该函数在OC运行时参考中也有说明,最后通过调用calloc 来分配内存块。这和前面讲述的GNUstep 的实现并无多大差异。class_createInstance 函数的源代码可以通过objc4 库中的runtime/objc-runtime-new.mm
进行确认。
retainCount/retain/release
实例方法又是怎样实现的呢?下面列出各个方法分别调用的方法和函数:
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
各个方法都通过同一个调用了__CFDoExternRefOperation函数
, 调用了一系列名称相似的函数。如这些函数名的前缀"CF"所示, 它们包含于Core Foundation框架源代码中,即是CFRuntime.c的__CFDoExternRefOperation
函数。为了理解其实现,下面是简化了__CFDoExternRefOperation
函数后的源代码:
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对象对应的散列表(obj);
int count;
switch (op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case OPERATION_retain:
CFBasicHashAddValue(table, obj);
return obj;
case OPERATION_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}
__CFDoExternRefOperation
函数按retainCount/retain/release
操作进行分发,调用不同的函数。NSObject
类的retainCount/retain/release
实例方法也许如下面代码所示:
- (NSUInteger)retainCount {
return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount, self);
}
- (id)retain {
return (id)__CFDoExternRefOperation(OPERATION_retain, self);
}
- (void)release {
return __CFDoExternRefOperation(OPERATION_release, self);
}
可以从__CFDoExternRefOperation
函数以及由此函数调用的各个函数名看出,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。
GNUstep将引用计数保存在对象占用内存块头部的变量中,而苹果的实现,则是保存在引用计数表的记录中。GNUstep的实现看起来既简单又高效,而苹果如此实现必然有它的好处。
通过内存块头部管理引用计数的好处如下:
- 少量代码即可完成。
- 能够统一管理引用计数用内存块与对象用内存块。
通过引用计数表管理引用计数的好处如下:
- 对象用内存块的分配无需考虑内存块头部。
- 引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象的内存块。
注意,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置。
另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。通过以上即可理解苹果的实现。
(四)autorelease
说到OC内存管理,就不得不说autorelease
。顾名思义,autorelease
就是自动释放。这看上去很像ARC,但实际上它更类似C语言中自动变量(局部变量)的特性。C语言的自动变量,若某自动变量超出其作用域,该自动变量将被自动放弃。
{
int a;
}
//超出变量作用域
//自动变量“int a”被废弃,不可再访问
autorelease会像C语言的自动变量那样来对待对象实例。当超出其作用域时,对象实例的release实例方法被调用。另外,与C的自动变量不同的是,我们可以设定变量的作用域。
autorelease的具体使用方法如下:
- 生成并持有NSAutoReleasePool对象;
- 调用已分配对象的autorelease实例方法;
- 废弃NSAutoreleasePool对象。
NSAutoreleasePool对象生存周期:
NSAutoreleasePool
对象生存周期相当于C语言变量的作用域。对于所有调用过autorelease
实例方法的对象,在废弃NSAutoreleasePool
对象时,都将调用release
实例方法。源代码:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];//or [obj release];
上述源代码中最后一行的[pool drain]
等同于[obj release]
。
在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。
尽管如此,但在大量产生autorelease
的对象时,只要不废弃NSAutoreleasePool
对象,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。典型的例子是读入大量图像的同时改变其尺寸。图像文件读入到NSData
对象,并从中生成UIImage
对象,改变该对象尺寸后生成新的UIImage
对象。这种情况下,就会大量产生autorelease
的对象。
for (int i = 0; i < 图像数; ++i) {
/*
* 读入图像
* 大量产生autorelease 的对象。
* 由于没有废弃NSAutoreleasePool 对象
* 最终导致内存不足!
*/
}
在此情况下,在有必要的地方生成、持有或废弃NSAutoreleasePool
对象。
for (int i = 0; i < 图像数; ++i) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/*
* 读入图像
* 大量产生autorelease 的对象。
*/
[pool drain];
/*
* 通过[pool drain],
* autorelease 的对象被一起release。
*/
}
另外,Cocoa框架中也有很多类方法用于返回autorelease
的对象。比如NSMutableArray
类的arrayWithCapacity:
类方法。
id array = [NSMutableArray arrayWithCapacity:1];
此代码等于以下源代码:
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
(五)autorelease实现
autorelease
是怎样实现的呢?为了加深理解,同alloc/retain/release/dealloc
一样,我们来查看一下GNUstep
的源代码。
[obj autorelease];
源代码:
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
autorelease
实例方法的本质就是调用NSAutoreleasePool
对象的addObject
类方法。
提高调用Objective-C方法的速度
GNUstep 中的autorelease 实际上是用一种特殊的方法来实现的。这种方法能够高效地运行OS X、iOS应用程序中频繁调用的
autorelease
方法,它被称为“IMP Caching”。在进行方法调用时,为了解决类名/ 方法名以及取得方法运行时的函数指针,要在框架初始化时对其结果值进行缓存。id autorelease_class = [NSAutoreleasePool class]; SEL autorelease_sel = @selector(addObject:); IMP autorelease_imp = [autorelease_class methodForSelector:autorelease_sel];
实际的方法调用就是使用缓存的结果值。
- (id) autorelease { (*autorelease_imp)(autorelease_class, autorelease_sel, self); }
这就是IMP Caching 的方法调用。虽然同以下源代码完全相同,但从运行效率上看,即使它依赖于运行环境,一般而言速度也是其他方法的2 倍。
- (id) autorelease { [NSAutoreleasePool addObject:self]; }
下面来看一下NSAutoreleasePool
类的实现。由于NSAutoreleasePool
类的源代码比较复杂,所以我们假想一个简化的源代码进行说明。
+ (void)addObject:(id)anObj {
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool 对象;
if (pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"NSAutoreleasePool 对象非存在状态下调用autorelease");
}
}
addObject
类方法调用正在使用的NSAutoreleasePool
对象的addObject
实例方法。以下源代码中,被赋予pool
变量的即为正在使用的NSAutoreleasePool
对象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
如果嵌套生成或持有的NSAutoreleasePool
对象,理所当然会使用最内侧的对象。下例中,pool2
为正在使用的NSAutoreleasePool
对象。
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];
下面看一下addObject实例方法的实现。
- (void)addObject:(id)anObj {
[array addObject:anObj];
}
实际上GNUstep实现使用的是连接列表,这同在NSMutableArray
对象中追加对象参数是一样的。
如果调用NSObject
类的autorelease
实例方法,该对象将被追加到正在使用的NSAutoreleasePool
对象中的数组里。
[pool drain];
以下为通过drain
实例方法废弃正在使用的NSAutoreleasePool
对象的过程。
- (void) drain {
[self dealloc];
}
- (void)dealloc {
[self emptyPool];
[array release];
}
- (void)emptyPool {
for (id obj in array) {
[obj release];
}
}
虽然调用了好几个方法,但可以确定对于数组中的所有对象都调用了release
实例方法。
(六)苹果的实现
可通过objc4库的runtime/objc-arr.mm来确认苹果中autorelease
的实现。
class AutoreleasePoolPage {
static inline void *push() {
相当于生成或持有NSAutoreleasePool类对象;
}
static inline void *pop() {
相当于废弃NSAutoreleasePool类对象;
releaseAll();
}
static inline id autorelease(id obj) {
相当于NSAutoreleasePool类的addObject类方法
AutorelesePoolPage *autorelesePoolPage = 取得正在使用的AutorelesePoolPage实例;
autorelesePoolPage->add(obj);
}
id *add(id obj) {
将对象追加到内部数组中;
}
void releaseAll() {
调用内部数组中对象的release实例方法;
}
};
void *objc_autorelesePoolPush(void) {
return AutorelesePoolPage::push();
}
void *objc_autorelesePoolPush(void *ctxt) {
return AutorelesePoolPage::pop(ctxt);
}
id *objc_autorelease(id obj) {
return AutoreleasePoolPage::autorelease(obj);
}
C++类中虽然有动态数组的实现,但其行为和GNUstep的实现完全相同。使用调试器来观察NSAutoreleasePool
类方法和autorelease
方法的运行过程,如下所示,这些方法调用了关联于objc4库autorelease
实现的函数。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//等同于objc_autoreleasePoolPush()
id obj = [[NSObject alloc] init];
[obj autorelease];
//等同于objc_autorelease(obj)
[pool drain];
//等同于objc_autoreleasePoolPop(pool)
另外,可通过NSAutoreleasePool
类中的调试用非公开类方法showPools
来确认已被autorelease
的对象的情况。showPools会将现在的NSAutoreleasePool
的状况输出到控制台。
[NSAutoreleasePool showPools];
NSAutoreleasePool
类的showPools
类方法只能在iOS中使用,在现在的运行时系统中,我们使用调试用非公开函数_objc_autoreleasePoolPrint()
。
//函数声明
extern void _objc_autotreleasePoolPrint();
//autoreleasepool调用输出开始
_objc_autoreleasePoolPrint();
如果运行此函数,就能在控制台中确认AutoreleasePoolPage
类的情况。
该函数在检查某对象是否被自动release时非常有用。
autorelease NSAutoreleasePool对象
如果autorelease NSAutoreleasePool对象会如何?
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [pool autorelease];
答:发生异常
通常在使用OC,也就是Foundation框架时,无论调用哪一个对象的autorelease实例方法,实际上是调用NSObject类的autorelease实例方法。但是对于NSAutorelease类,autorelease实例方法已被该类重载,因此运行就会出错
三、ARC规则
(一)概要
实际上“引用计数式内存管理”的本质部分在ARC 中并没有改变。就像“自动引用计数”这个名称表示的那样,ARC 只是自动地帮助我们处理“引用计数”的相关部分。
在编译单位上,可设置ARC 有效或无效,这一点便能佐证上述结论。比如对每个文件可选择使用或不使用ARC。
(二)内存管理的思考方式
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 自己持有的对象不再需要时释放。
- 非自己持有的对象无法释放。
这一思考方式在ARC 有效时也是可行的。只是在源代码的记述方法上稍有不同。到底有什么样的变化呢?首先要理解ARC 中追加的所有权声明。
(三)所有权修饰符
OC编程中为了处理对象,可将变量类型定义为id 类型或各种对象类型。所谓对象类型就是指向NSObject
这样的OC类的指针,例如“NSObject *
”。id
类型用于隐藏对象类型的类名部分,相当于C 语言中常用的“void *
”。
ARC 有效时,id
类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符。所有权修饰符一共有4 种。
- __strong 修饰符
- __weak修饰符
- __unsafe_unretained修饰符
- __autoreleasing修饰符
1. __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 超出其作用域,强引用失效,
* 所以自动地释放自己持有的对象。
* 对象的所有者不存在,因此废弃该对象。
*/
此处,对象的所有者和对象的生存周期是明确的。那么,在取得非自己生成并持有的对象时又会如何呢?
{
id __strong obj = [NSMutableArray array];
}
在NSMutableArray 类的array 类方法的源代码中取得非自己生成并持有的对象,具体如下:
{
/*
* 取得非自己生成并持有的对象
*/
id __strong obj = [NSMutableArray array];
/*
* 因为变量obj 为强引用,
* 所以自己持有对象
*/
} /*
* 因为变量obj 超出其作用域,强引用失效,
* 所以自动地释放自己持有的对象
* /
在这里对象的所有者和对象的生存周期也是明确的。
{
/*
* 自己生成并持有对象
*/
id __strong obj = [[NSObject alloc] init];
/*
* 因为变量obj 为强引用,
* 所以自己持有对象
*/
} /*
* 因为变量obj 超出其作用域,强引用失效,
* 所以自动地释放自己持有的对象。
* 对象的所有者不存在,因此废弃该对象。
*/
当然,附有__strong
修饰符的变量之间可以相互赋值。
id __strong obj0 = [[NSObject alloc] init];
id __strong obj1 = [[NSObject alloc] init];
id __strong obj2 = nil;
obj0 = obj1;
obj2 = obj0;
obj1 = nil;
obj0 = nil;
obj2 = nil;
下面来看一下生成并持有对象的强引用。
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 被赋予obj0, 所以对对象B 的强引用失效。
*
* 此时,持有对象B 的强引用的变量为
* obj2。
*/
obj2 = nil;
/*
* 因为nil 被赋予obj2, 所以对对象B 的强引用失效。
* 对象B 的所有者不存在,因此废弃对象B。
*/
通过上面这些不难发现,__strong
修饰符的变量,不仅只在变量作用域中,在赋值上也能够正确地管理其对象的所有者。
当然,即便是OC类成员变量,也可以在方法参数上,使用附有__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 {
objobj_ = obj;
}
@end
下面试着使用该类:
{
id __strong test = [[Test alloc] init];
[test setObject:[[NSObject alloc] init]];
}
该例中生成并持有对象的状态记录如下:
{
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
。
id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;
以下源代码和上面相同:
id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;
正如苹果宣称的那样,通过__strong
修饰符,不必再次键入retain
或者release
,满足了“引用计数式内存管理的思考方式”:
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 自己持有的对象不再需要时释放。
- 非自己持有的对象无法释放。
前两项“自己生成的对象,自己持有”和“非自己生成的对象,自己也能持有”只需通过对带__strong
修饰符的变量赋值便可达成。通过废弃带 __strong
修饰符的变量(变量作用域结束或是成员变量所属对象废弃)或者对变量赋值,都可以做到“不再需要自己持有的对象时释放”。最后一项“非自己持有的对象无法释放”,由于不必再再次键入release
,所以原本就不会执行。这些都满足于引用计数式内存管理的思考方式。
因为id
类型和对象类型的所有权修饰符默认为__strong
修饰符,所以不需要写上“__ strong
”。使ARC有效及简单的编程遵循了OC内存管理的思考方式。
2. __weak修饰符
看起来好像通过__strong
修饰符编译器就可以完美地进行内存管理,但遗憾的是,仅通过__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 test0 = [[Test alloc] init];
id test1 = [[Test alloc] init];
[test0 setObject:test1];
[test1 setObject:test0];
}
为便于理解,下面写出了生成并持有对象的状态。
{
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_。
*
* 发生内存泄漏!
*
*/
循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。
此代码的本意是赋予变量test0 的对象A 和赋予变量test1 的对象B 在超出其变量作用域时被释放,即在对象不被任何变量持有的状态下予以废弃。但是,循环引用使得对象不能被再次废弃。
像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)。
id test = [[Test alloc] init];
[test setObject:test];
怎么样才能避免循环引用呢?看到__strong
修饰符就会意识到了,既然有strong
,就应该有与之对应的weak
。也就是说,使用__weak
修饰符可以避免循环引用。
__weak
修饰符与__strong
修饰符相反,提供弱引用。弱引用不能持有对象实例。我们来看看下面的代码。
id __weak obj = [[NSObject alloc] init];
变量obj
上附加了__weak
修饰符。实际上如果编译以下代码,编译器会发出警告。
此源代码将自己生成并持有的对象赋值给附有__weak
修饰符的变量obj
。即变量obj
持有对持有对象的弱引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会立即被释放。编译器对此会给出警告。如果像下面这样,将对象赋值给附有__strong
修饰符的变量之后再赋值给附有__weak
修饰符的变量,就不会发生警告了。
{
id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
}
下面确认对象的持有状况。
{
/*
* 自己生成并持有对象
*/
id __strong obj0 = [[NSObject alloc] init];
/*
* 因为obj0 变量为强引用,
* 所以自己持有对象。
*/
id __weak obj1 = obj0;
/*
* obj1 变量持有生成对象的弱引用
*/
} /*
* 因为obj0 变量超出其作用域,强引用失效,
* 所以自动释放自己持有的对象。
* 因为对象的所有者不存在,所以废弃该对象。
*/
因为带__weak
修饰符的变量(即弱引用)不持有对象,所以在超出其变量作用域时,对象即被释放。如果像下面这样将先前可能发生循环引用的类成员变量改成附有__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];
obj1 = obj0;
NSLog(@"A: %@", obj1);
}
NSLog(@"B: %@", obj1);
此源代码执行结果如下:
A: <NSObject: 0x753e180>
B: (null)
下面我们来确认一下对象的持有情况,看看为什么会得到这样的执行结果。
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
*/
像这样,使用__weak
修饰符可避免循环引用。通过检查附有__weak
修饰符的变量是否为nil
,可以判断被赋值的对象是否已废弃。
3. __unsafe_unretained修饰符
__unsafe_unretained
修饰符正如其名unsafe
所示,是不安全的所有权修饰符。尽管ARC式的内存管理是编译器的工作,但附有__unsafe_unretained
修饰符的变量不属于编译器的内存管理对象。这一点在使用时要注意。
id __unsafe_unretained obj = [[NSObject alloc] init];
该源代码将自己生成并持有的对象赋值给附有__unsafe_unretained
修饰符的变量中。虽然使用了unsafe的变量,但编译器并不会忽略,而是给出适当的警告。
warning: assigning retained obj to unsafe_unretained variable;
obj will be released after assignment [-Warc-unsafe-retained-assign]
id __unsafe_unretained obj = [[NSObject alloc] init];
附有__unsafe_unretained 修饰符的变量同附有__weak 修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。到这里,__unsafe_unretained修饰符和__weak修饰符是一样的,下面我们来看看源代码的差异。
id __unsafe_unretained obj1 = nil;
{
id __strong obj0 = [[NSObject alloc] init];
obj1 = obj0;
NSLog(@"A: %@", obj1);
}
NSLog(@"B: %@",obj1);
该源代码的执行结果为:
A: <NSObject: 0x100518b20>
B: <NSObject: 0x100518b20>
我们还像以前那样,通过确认对象的持有情况来理解发生了什么。
id __unsafe_unretained obj1 = nil;
{
/*
* 自己生成并持有对象
*/
id __strong_obj0 = [[NSObject alloc] init];
/*
* 因为obj0变量为强引用,
* 所以自己持有对象。
*/
obj1 = obj0;
/*
* 虽然objo变量赋值给obj1,
* 但是obj1变量既不持有对象的强引用也不持有弱引用
*/
NSLog(@"A:*@",obj1);
/*
* 输出obj1变量表示的对象
*/
)
/*
* 因为objo变量超出其作用域,强引用失效,
* 所以自动释放自己持有的对象。
* 因为对象无持有者,所以废弃该对象。
*/
NSLog(@"B:@",obj1);
/*
* 输出obj1变量表示的对象
* obj1变量表示的对象
* 已经被废弃(悬垂指针)!
* 错误访问!
*/
也就是说,最后一行的NSLog
只是碰巧正常运行而已。虽然访问了已经被废弃的对象,但应用程序在个别运行状况下才会崩溃。
在使用__unsafe_unretained
修饰符时,赋值给附有__strong
修饰符的变量时有必要确保被赋值的对象确实存在。
但是,在使用前,让我们再一次想想为什么需要使用附有__unsafe_unretained
修饰符的变量。比如在iOS4以及OS X Snow Leopard的应用程序中,必须使用__unsafe_unretained
修饰符来替代__weak
修饰符。赋值给附有__unsafe_unretained
修饰符变量的对象在通过该变量使用时,如果没有确保其确实存在,那么应用程序就会崩溃。
4. __autoreleasing 修饰符
ARC有效时autorelease
会如何呢?实际上,不能使用autorelease
方法。另外,也不能使用NSAutoreleasePool
类。这样一来,虽然autorelease
无法直接使用,但实际上,ARC有效时autorelease
功能是起作用的。
ARC无效时会像下面这样来使用:
/* ARC无效*/
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
ARC有效时,该源代码也能写成下面这样:
Cautoreleasepool(
id_autoreleasing obj =[NsObject alloc] init];}
==指定“@autoreleasepool 块”==来替代“NSAutoreleasePool 类对象生成、持有以及废弃”这一范围。另外,ARC有效时,要通过将对象赋值给附加了__autoreleasing
修饰符的变量来替代调用 autoreleasc
方法。对象赋值给附有__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];
}
我们再来看看该源代码中对象的所有状况。
@autoreleasepool {
/*
* 取得非自己生成并持有的对象
*/
id __strong obj = [NSMutableArray array];
/*
* 因为变量obj为强引用,
* 所以自己持有对象。
*
* 并且该对象
* 由编译器判断其方法名后
* 自动注册到autoreleasepool
*/
}
/*
* 因为变量obj超出其作用域,强引用失效,
* 所以自动释放自己持有的对象。
*
* 同时,随着 @autoreleasepool块的结束,
* 注册到autoreleasepool中的所有对象被自动释放。
*
* 因为对象的所有者不存在,所以废弃对象。
*/
像这样,不使用__autoreleasing
修饰符也能使对象注册到autoreleasepool
。以下为取得非自己生成并持有对象时被调用方法的源代码示例。
+ (id)array
{
return [[NSMutableArray alloc] init];
}
该源代码也没有使用__autoreleasing
修饰符,可写成以下形式。
+ (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;
NSLog(@"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 **obi
便成为了 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:MyAppDomaincode:errorCode userInfo:nil];
return NO;
}
因为声明为NSError * __autoreleasing *
类型的error
作为*error
被赋值,所以能够返回注册到autoreleasepool
中的对象。
然而,下面的源代码会产生编译器错误。
NSError *error = nil;
NSError **pError = &error;
//initializing 'NSError *__autoreleasing *'
//with an expression of type 'NSError *__strong *'
赋值给对象指针时,所有权修饰符必须一致。
error: initializing 'NSError *__autoreleasing *' with an expression of type 'NSError *__strong *' changes retain/release properties of pointer
NSError **pError = &error;
^ ~~~~~~
此时,对象指针必须附加__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
修饰符,但在显式地指定__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 中的对象。
另外,如果编译器版本为LLVM 3.0 以上,即使ARC 无效@autoreleasepool 块也能够使用,如以下源代码所示。
/* ARC 无效 */
@autoreleasepool {
id obj = [[NSObject alloc] init];
[obj autorelease];
}
因为autoreleasepool
范围以块级源代码表示,提高了程序的可读性,所以今后在ARC 无效时也推荐使用@autoreleasepool 块。
另外,无论ARC 是否有效,调试用的非公开函数_objc_autoreleasePoolPrint()
都可使用。
_objc_autoreleasePoolPrint();
利用这一函数可有效地帮助我们调试注册到autoreleasepool
上的对象。
(四)规则
在ARC 有效的情况下编译源代码,必须遵守一定的规则。下面就是具体的ARC 的规则。
- 不能使用retain/release/retainCount/autorelease
- 不能使用NSAllocateObject/NSDeallocateObject
- 须遵守内存管理的方法命名规则
- 不要显式调用dealloc
- 使用@autoreleasepool块替代NSAutoreleasePool
- 不能使用区域(NSZone)
- 对象型变量不能作为C语言结构体(struct/union)的成员
- 显式转换“id”和“void *”
下面详细解释各项。
1. 不能使用retain/release/retainCount/autorelease
内存管理是编译器的工作,因此没有必要使用内存管理的方法(retain/release/retainCount/autorelease)。以下摘自苹果的官方说明。
“设置ARC 有效时,无需再次键入retain 或release 代码。”
实际上,在ARC 有效时,如果编译器使用了这些方法的源代码,就会出现如下错误:
error: ARC forbids explicit message send of 'release'
[o release];
^ ~~~~~~~
一旦使用便会出现编译错误,因此可更准确地描述为:
“设置ARC 有效时,禁止再次键入retain 或者是release 代码。”
retainCount
和release
都会引起编译错误,因此不能使用以下代码。
for (;;) {
NSUInteger count = [obj retainCount];
[obj release];
if (count == 1)
break;
}
ARC 被设置为无效时,该源代码也完全不符合引用计数式内存管理的思考方式,也就是说它在任何情况下都无法使用,所以没有问题。
总之,只能在ARC 无效且手动进行内存管理时使用retain/release/retainCount/autorelease
方法。
2. 不能使用NSAllocateObject/NSDeallocateObject
一般通过调用NSObject
类的alloc
类方法来生成并持有OC 对象。
id obj = [NSObject alloc];
但是就如GNUstep 的alloc
实现所示,实际上是通过直接调用NSAllocateObject
函数来生成并持有对象的。
在ARC 有效时,禁止使用NSAllocateObject
函数。同retain
等方法一样,如果使用便会引起编译错误。
error: 'NSAllocateObject' is unavailable:
not available in automatic reference counting mode
同样地,也禁止使用用于释放对象的NSDeallocateObject 函数。
3. 须遵守内存管理的方法命名规则
在ARC 无效时,用于对象生成/持有的方法必须遵守以下的命名规则。
- alloc
- new
- copy
- mutableCopy
以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC 有效时也一样,返回的对象完全没有改变。只是在ARC 有效时要追加一条命名规则。
(1)init
以init
开始的方法的规则要比alloc/new/copy/mutableCopy
更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应为id
类型或该方法声明类的对象类型,抑或是该类的超类型或子类型。该返回对象并不注册到autoreleasepool 上。基本上只是对alloc
方法返回值的对象进行初始化处理并返回该对象。
以下为使用该方法的源代码。
id obj = [[NSObject alloc] init];
如此源代码所示,init
方法会初始化alloc
方法返回的对象,然后原封不动地返还给调用方。
下面我们来看看以init
开始的方法的命名规则。
- (id)initWithObject:(id)obj;
该方法声明遵守了命名规则,但像下面这个方法虽然也以init
开始,却没有返回对象,因此不能使用。
- (void)initThisObject;
另外,下例虽然也是以init 开始的方法但并不包含在上述命名规则里。请注意。
- (void)initialize;
4. 不要显式调用dealloc
无论ARC 是否有效,只要对象的所有者都不持有该对象,该对象就被废弃。对象被废弃时,不管ARC 是否有效,都会调用对象的dealloc
方法。
- (void)dealloc {
/*
* 此处运行该对象被废弃时
* 必须实现的代码
*/
}
例如使用C 语言库,在该库内部分配缓存时,如以下所示,dealloc
方法需要通过free
来释放留出的内存。
- (void)dealloc {
free(buffer_);
}
dealloc
方法在大多数情况下还适用于删除已注册的代理或观察者对象。
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
另外,在ARC 无效时必须像下面这样调用[super dealloc]
。
/* ARC 无效 */
- (void)dealloc {
/* 该对象用的处理 */
[super dealloc];
}
ARC 有效时会遵循无法显式调用dealloc
这一规则,如果使用就会同release
等方法一样,引起编译错误。
error: ARC forbids explicit message send of 'dealloc'
[super dealloc];
^ ~~~~~~~
ARC 会自动对此进行处理,因此不必书写[super dealloc]
。dealloc
中只需记述废弃对象时所必需的处理。
5. 使用@autoreleasepool 块替代NSAutoreleasePool
如__autoreleasing
修饰符项所述,ARC 有效时,使用@autoreleasepool 块替代NSAutoreleasePool。
NSAutoreleasePool 类不可使用时便会引起编译器报错。
6. 不能使用区域(NSZone)
虽说ARC 有效时,不能使用区域(NSZone)。正如前所述,不管ARC 是否有效,区域在现在的运行时系统(编译器宏__OBJC2__
被设定的环境)中已单纯地被忽略。
7. 对象型变量不能作为C 语言结构体的成员
C 语言的结构体(struct 或union)成员中,如果存在OC 对象型变量,便会引起编译错误。
struct Data {
NSMutableArray *array;
};
error: ARC forbids Objective-C objs in structs or unions
NSMutableArray *array;
C 语言的规约上没有方法来管理结构体成员的生存周期a。因为ARC把内存管理的工作分配给编译器,所以编译器必须能够知道并管理对象的生存周期。例如C 语言的自动变量(局部变量)可使用该变量的作用域管理对象。但是对于C 语言的结构体成员来说,这在标准上就是不可实现的。
要把对象型变量加入到结构体成员中时,可强制转换为void *
或是附加前面所述的__unsafe_unretained
修饰符。
struct Data {
NSMutableArray __unsafe_unretained *array;
};
如前所述,附有__unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便有可能遭遇内存泄漏或程序崩溃。这点在使用时应多加注意。
8. 显式转换 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 conversion of an Objective-C pointer to 'void *' is disallowed with ARC
void *p = obj;
^
error: implicit conversion of a non-Objective-C pointer type 'void *' to 'id' is disallowed with ARC
id o = p;
^
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
转换可使要转换赋值的变量也持有所赋值的对象。下面我们来看ARC 无效时的源代码是如何编写的。
/* ARC 无效 */
id obj = [[NSObject alloc] init];
void *p = obj;
[(id)p retain];
__bridge_retained
转换变为了retain
。变量obj
和变量p
同时持有对象。再来看几个其他的例子。
void *p = 0;
{
id obj = [[NSObject alloc] init];
p = (__bridge_retained void *)obj;
}
NSLog(@"class=%@", [(__bridge id)p class]);
变量作用域结束时,虽然随着持有强引用的变量obj
失效,对象随之释放,但由于__bridge_retained
转换使变量p
看上去处于持有该对象的状态,因此该对象不会被废弃。下面我们比较一下ARC 无效时的代码是怎样的。
/* 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
* 对象仍存在
*/
NSLog(@"class=%@", [(__bridge id)p class]);
__bridge_transfer
转换提供与此相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随之释放。
id obj = (__bridge_transfer id)p;
该源代码在ARC无效时又如何表述呢?
/*ARC无效*/
id obj = (id)p;
[obj retain];
[(id)p release];
同__bridge_retained
转换与 retain
类似,__bridge_transfer
转换与 release
相似。在给 id obj
赋值时 retain
即相当于__strong
修饰符的变量。
如果使用以上两种转换,那么不使用id
型或对象型变量也可以生成、持有以及释放对象。虽然可以这样做,但在ARC中并不推荐这种方法。使用时还请注意。
void *p = (__bridge_retained void *)[[NSObject alloc] init];
NSLog(@"class=%@",[(bridge id)p class]);
(void)(__bridge_transfer id)p;
该源代码与ARC无效时的下列源代码相同。
/* ARC 无效 */
id p = [[NSObject alloc] init];
NSLog(@"class=%@",[p class]);
[p release];
这些转换多数使用在 Objective-C 对象与Core Foundation 对象之间的相互变换中。
专栏objective-c对象与Core Foundation对象
Core Foundation 对象主要使用在用C语言编写的Core Foundation 框架中,并使用引用计数的对象。在ARC无效时,Core Foundation 框架中的retain/release 分别是CFRetain/CFRelease
Core Foundation 对象与Objective-c 对象的区别很小,不同之处只在于是由哪一个框架(Foundation 框架还是 Core Foundation 框架)所生成的。无论是由哪种框架生成的对象,一旦生成之后,便能在不同的框架中使用。Foundation 框架的API 生成并持有的对象可以用 Core Foundation 框架的API释放。当然,反过来也是可以的。
因为Core Foundation对象与Objective-C对象没有区别,所以在ARC无效时,只用简单的C语言的转换也能实现互换。另外这种转换不需要使用额外的CPU资源,因此也被称为“免费桥”(Toll-Free Bridge )。
以下函数可用于Objective-C对象与Core Foundation 对象之间的相互变换,即Toll-Free Bridge 转换。
CFTypeRef CFBridgingRetain(id X) {
return (__bridge_retained CFTypeRef)X;
}
id CFBridgingRelease(CFTypeRef X)(
return (__bridge_transfer id)X;
}
我们来看看到底是如何使用的。以下将生成并持有的NSMutableArray对象作为Core Foundation 对象来处理。
CFMutableArrayRef cfObject = NULL;
{
id obj = [[NSMutableArray alloc] init];
cfObject = CFBridgingRetain(obj);
CFShow(cfObject);
printf("retain count = %d\n", CFGetRetainCount(cfobject));
}
printf("retain count after the scope = %d\n", CFGetRetainCount(cfObject));
CFRelease(cfObject);
该源代码正常运行后,会输出以下结果。()表示空的数组。
(
)
retain count = 2
retain count after the scope = 1
由此可知,Foundation 框架的API 生成并持有的OC 对象能够作为Core Foundation对象来使用。也可以通过CFRelease
来释放。当然,也可以使用__bridge_retained
转换来替代CFBridgingRetain
。大家可选用自己更熟悉的方法。
CFMutableArrayRef cfObject = (__bridge_retained CFMutableArrayRef)obj;
以下基于CFGetRetainCount
的值来确认对象的所有状况。
CFMutableArrayRef cfObject = NULL;
{
id obj = [[NSMutableArray alloc] init];
/*
* 变量obj 持有对生成并持有对象的强引用。
*/
cfObject = CFBridgingRetain(obj);
/*
* 通过CFBridgingRetain,
* 将对象CFRetain,
* 赋值给变量cfObject。
*/
CFShow(cfObject);
printf("retain count = %d\n",CFGetRetainCount(cfObject));
/*
* 通过变量obj 的强引用和
* 通过CFBridgingRetain,
* 引用计数为2。
*/
} /*
* 因为变量obj 超出其作用域,所以其强引用失效,
* 引用计数为1。
*/
printf("retain count after the scope = %d\n", CFGetRetainCount(cfObject));
CFRelease(cfObject);
/*
* 因为将对象CFRelease,所以其引用计数为0,
* 故该对象被废弃。
*/
使用__bridge 转换来替代CFBridgingRetain 或__bridge_retained 转换时,源代码会变成什么样呢?
CFMutableArrayRef cfObject = NULL;
{
id obj = [[NSMutableArray alloc] init];
/*
* 变量obj 持有对生成并持有对象的强引用。
*/
cfObject = (__bridge CFMutableArrayRef)obj;
CFShow(cfObject);
printf("retain count = %d\n",CFGetRetainCount(cfObject));
/*
* 因为__bridge 转换不改变对象的持有状况,
* 所以只有通过变量obj 的强引用,
* 引用计数为1。
*/
} /*
* 因为变量obj 超出其作用域,
* 所以其强引用失效,对象得到释放,
* 无持有者的对象被废弃。
*/
/*
* 此后对对象的访问出错!(悬垂指针)
*/
printf("retain count after the scope = %d\n", CFGetRetainCount(cfObject));
CFRelease(cfObject);
由此可知,CFBridgingRetain
或者__bridge_retained
转换是不可或缺的。
这次反过来,将使用Core Foundation 的API 生成并持有对象,将该对象作为NSMutableArray对象来处理。
{
CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
printf("retain count = %d\n", CFGetRetainCount(cfObject));
id obj = CFBridgingRelease(cfObject);
printf("retain count after the cast = %d\n", CFGetRetainCount(cfObject));
NSLog(@"class=%@", obj);
}
由此可知,与之前相反的由Core Foundation 框架的API 生成并持有的Core Foundation 对象也能够作为OC 对象来使用。其运行结果如下:
retain count = 1
retain count after the cast = 1
当然也可使用__bridge_transfer
转换替代CFBridgingRelease。
id obj = (__bridge_transfer id)cfObject;
此处也要基于CFGetRetainCount
的值来确认对象的持有状况。
{
CFMutableArrayRef cfObject =
CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
printf("retain count = %d\n", CFGetRetainCount(cfObject));
/*
* Core Foundation 框架的API 生成并持有对象
* 之后的对象引用计数为“1”。
*/
id obj = CFBridgingRelease(cfObject);
/*
* 通过CFBridgingRelease 赋值,
* 变量obj 持有对象强引用的同时
* 对象通过CFRelease 释放。
*/
printf("retain count after the cast = %d\n", CFGetRetainCount(cfObject));
/*
* 因为只有变量obj
* 持有对生成并持有对象的强引用,
* 故引用计数为“1”。
*
* 另外,因为经由CFBridgingRelease 转换后,
* 赋值给变量cfObject 中的指针
* 也指向仍然存在的对象,
* 所以可以正常使用。
*/
NSLog(@"class=%@", obj);
} /*
* 因为变量obj 超出其作用域,
* 所以其强引用失效,对象得到释放,
* 无所有者的对象随之被废弃。
*/
以下为用__bridge
转换替代CFBridgingRelease
或__bridge_transfer
转换的情形。
{
CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
printf("retain count = %d\n", CFGetRetainCount(cfObject);
/*
* Core Foundation 框架生成并持有对象
* 之后的对象引用计数为“1”。
*/
id obj = (__bridge id)cfObject;
/*
* 因为赋值给附有__strong 修饰符的变量中,
* 所以发生强引用。
*/
printf("retain count after the cast = %d\n", CFGetRetainCount(cfObject));
/*
* 因为变量obj 持有对象强引用且
* 对象没有进行CFRelease,
* 所以引用计数为“2”。
*/
NSLog(@"class=%@", obj);
} /*
* 因为变量obj 超出其作用域,
* 所以其强引用失效,对象得以释放。
*/
/*
* 因为引用计数为“1”,所以对象仍然存在。
* 发生内存泄漏!
*/
因此, 必须恰当使用CFBridgingRetain
/CFBridgingRelease
或者__bridge_retained
/__bridge_transfer
转换。在将OC变量赋值给C 语言变量,即没有附加所有权修饰符的void *
等指针型变量时,伴随着一定的风险。在实现代码时要高度重视。
(五)属性
当ARC 有效时,OC类的属性也会发生变化。
@property (nonatomic, strong) NSString *name;
当ARC 有效时,以下可作为这种属性声明中使用的属性来用。
属性声明的属性 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained修饰符 |
copy | __strong修饰符(但赋值的是被复制的对象) |
retain | __strong修饰符 |
strong | __strong修饰符 |
unsafe_unretained | __unsafe_unretained修饰符 |
weak | __weak修饰符 |
以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。只有copy
属性不是简单的赋值,它赋值的是通过NSCopying
接口的copyWithZone:
方法复制赋值源所生成的对象。
另外,在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误。比如下面这种情况。
id obj;
在声明id
型obj
成员变量时,像下面这样,定义其属性声明为weak
。
@property (nonatomic, weak) id obj;
编译器出现如下错误。
error: existing ivar 'obj' for __weak property 'obj' must be __weak
@synthesize obj;
^
note: property declared here
@property (nonatomic, weak) id obj;
此时,成员变量的声明中需要附加__weak
修饰符。
id __weak obj;
或者使用strong
属性替代weak
属性。
@property (nonatomic, strong) id obj;
(六)数组
以下是将附有__strong
修饰符的变量作为静态数组使用的情况。
id objs[10];
__weak
修饰符,__autoreleasing
修饰符以及__unsafe_unretained
修饰符也与此相同。
id __weak objs[10];
__unsafe_unretained
修饰符以外的__strong
/__weak
/__autoreleasing
修饰符保证其指定的变初始化为nil
。同样地,附有__strong
/__weak
/__autoreleasing
修饰符变量的数组也保证其初始化为nil
。下面我们就来看看数组中使用附有__strong
修饰符变量的例子。
{
id objs[2];
objs[0] = [[NSObject alloc] init];
objs[1] = [NSMutableArray array];
}
数组超出其变量作用域时,数组中各个附有__strong
修饰符的变量也随之失效,其强引用消失,所赋值的对象也随之释放。这与不使用数组的情形完全一样。
将附有__strong
修饰符的变量作为动态数组来使用时又如何呢?在这种情况下,根据不同的目的选择使用NSMutableArray、NSMutableDictionary、NSMutableSet 等 Foundation 框架的容器。这些容器会恰当地持有追加的对象并为我们管理这些对象。
像这样使用容器虽然更为合适,但在C语言的动态数组中也可以使用附有strong
修饰符的变量,只是必须要遵守一些事项。以下按顺序说明。
声明动态数组用指针。
id __strong *array = nil;
如前所述,由于“id*
类型”默认为“id __autoreleasing*
类型”,所以有必要显式指定为__strong
修饰符。另外,虽然保证了附有__strong
修饰符的id型变量被初始化为nil
,但并不保证附有__strong
修饰符的 id
指针型变量被初始化为nil
。
另外,使用类名时如下记述。
NSObject* __strong *array = nil;
其次,使用calloc
函数确保想分配的附有__strong
修饰符变量的容量占有的内存块。
array = (id __strong *)calloc(entries, sizeof(id));
该源代码分配了 entries
个所需的内存块。由于使用附有__strong
修饰符的变量前必须先将其初始化为 nil
,所以这里使用使分配区域初始化为0的calloc
函数来分配内存。不使用calloc
函数,在用malloc
函数分配内存后可用memset
等函数将内存填充为0。
但是,像下面的源代码这样,将nil
代入到malloc
函数所分配的数组各元素中来初始化是非常危险的。
array = (id __strong *)malloc(sizeof(id) * entries);
for (NSUInteger i = O; i < entries; ++i) {
array[i] = nil;
}
这是因为由malloc
函数分配的内存区域没有被初始化为0,因此nil
会被赋值给附有__strong
修饰符的并被赋值了随机地址的变量中,从而释放一个不存在的对象。在分配内存时推荐使用 calloc
函数。
像这样,通过calloc
函数分配的动态数组就能完全像静态数组一样使用。
array[0] = [[NSObject alloc] init];
但是,在动态数组中操作附有___strong
修饰符的变量与静态数组有很大差异,需要自己释放所有的元素。
如以下源代码所示,在只是简单地用free
函数废弃了数组用内存块的情况下,数组各元素所赋值的对象不能再次释放,从而引起内存泄漏。
free(array);
这是因为在静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理。如以下源代码所示,一定要将 nil
赋值给所有元素中,使得元素所赋值对象的强引用失效,从而释放那些对象。在此之后,使用 free
函数废弃内存块。
for(NSUInteger i = 0; i < entries; ++i) {
array[i] = nil;
}
free(array);
同初始化时的注意事项相反,即使用memset
等函数将内存填充为0也不会释放所赋值的对象。这非常危险,只会引起内存泄漏。对于编译器,必须明确地使用赋值给附有__strong修饰符变量的源代码。所以请注意,必须将 nil
赋值给所有数组元素。
另外,使用memcpy
函数拷贝数组元素以及realloc
函数重新分配内存块也会有危险。由于数组元素所赋值的对象有可能被保留在内存中或是重复被废弃,所以这两个函数也禁止使用。
再者,我们也可以像使用__strong
修饰符那样使用附有__weak
修饰符变量的动态数组。在 autoreleasing
修饰符的情况下,因为与设想的使用方法有差异,所以最好不要使用动态数组。由于__unsafe_unretained
修饰符在编译器的内存管理对象之外,所以它与 void*
类型一样,只能作为C语言的指针类型来使用。
四、ARC的实现
苹果的官方说明中称,ARC 是“由编译器进行内存管理”的,但实际上只有编译器是无法完全胜任的,在此基础上还需要OC运行时库的协助。也就是说,ARC 由以下工具、库来实现。
- clang(LLVM编译器)3.0 以上
- objc4 Objective-C 运行时库 493.9以上
(一)__strong修饰符
赋值给附有__strong
修饰符的变量在实际的程序中到底是怎样运行的呢?
{
id __strong obj = [[NSObject alloc] init];
}
看汇编输出和objc4库的源代码就能够知道程序是如何工作的。该源代码实际上可转换为调用以下的函数。为了便于理解,以后的源代码有时也使用模拟源代码。
/* 编译器的模拟代码 */
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
如原源代码所示,2 次调用objc_msgSend
方法(alloc
方法和init
方法),变量作用域结束时通过objc_release
释放对象。虽然ARC 有效时不能使用release
方法,但由此可知编译器自动插入了release
。下面我们来看看使用alloc
/new
/copy
/mutableCopy
以外的方法会是什么情况。
{
id __strong obj = [NSMutableArray array];
}
调用了我们熟知的NSMutableArray 类的array
类方法,得到的结果却与之前稍有不同。
/* 编译器的模拟代码 */
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
虽然最开始的array 方法的调用以及最后变量作用域结束时的release
与之前相同,但中间的objc_retainAutoreleasedReturnValue
函数是什么呢?
objc_retainAutoreleasedReturnValue
函数主要用于最优化程序运行。顾名思义,它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool
中对象的方法,或是函数的返回值。像该源代码这样,在调用alloc
/new
/copy
/mutableCopy
以外的方法,即NSMutableArray 类的array
类方法等调用之后,由编译器插入该函数。
这种objc_retainAutoreleasedReturnValue
函数是成对的, 与之相对的函数是objc_autoreleaseReturnValue
。它用于alloc
/new
/copy
/mutableCopy
方法以外的NSMutableArray 类的array
类方法等返回对象的实现上。下面我们看看NSMutableArray 类的array
类通过编译器会进行怎样的转换。
+ (id)array {
return [[NSMutableArray alloc] init];
}
以下为该源代码的转换,转换后的源代码使用了objc_autoreleaseReturnValue
函数。
/* 编译器的模拟代码 */
+ (id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
像该源代码这样, 返回注册到autoreleasepool
中对象的方法使用了objc_autoreleaseReturnValue
函数返回注册到autoreleasepool
中的对象。但是objc_autoreleaseReturnValue
函数同objc_autorelease
函数不同,一般不仅限于注册对象到autoreleasepool
中。
objc_autoreleaseReturnValue
函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnValue( )
函数,那么就不将返回的对象注册到autoreleasepool
中,而是直接传递到方法或函数的调用方。objc_retainAutoreleasedReturnValue
函数与objc_retain
函数不同,它即便不注册到autoreleasepool
中而返回对象, 也能够正确地获取对象。通过objc_autoreleaseReturnValue
函数和objc_retainAutoreleasedReturnValue
函数的协作,可以不将对象注册到autoreleasepool
中而直接传递,这一过程达到了最优化。
(二)__weak修饰符
就像前面我们看到的一样
- 若附有
__weak
修饰符的变量所引用的对象被废弃,则将nil 赋值给该变量。 - 使用附有
__weak
修饰符的变量,即是使用注册到autoreleasepool
中的对象。
{
id __weak objobj1 = obj;
}
假设变量obj
附加__strong
修饰符且对象被赋值。
/* 编译器的模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
objc_destroyWeak(&obj1);
通过objc_initWeak
函数初始化附有__weak
修饰符的变量,在变量作用域结束时通过objc_destroyWeak
函数释放该变量。
如以下源代码所示,objc_initWeak
函数将附有__weak
修饰符的变量初始化为0 后,会将赋值的对象作为参数调用objc_storeWeak
函数。
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_destroyWeak
函数将0 作为参数调用objc_storeWeak
函数。
objc_storeWeak(&obj1, 0);
即前面的源代码与下列源代码相同。
/* 编译器的模拟代码 */
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(&obj1, 0);
objc_storeWeak
函数把第二参数的赋值对象的地址作为键值,将第一参数的附有__weak
修饰符的变量的地址注册到weak
表中。如果第二参数为0,则把变量的地址从weak
表中删除。
weak
表与引用计数表相同,作为散列表被实现。如果使用weak
表,将废弃对象的地址作为键值进行检索,就能高速地获取对应的附有__weak
修饰符的变量的地址。另外,由于一个对象可同时赋值给多个附有__weak
修饰符的变量中,所以对于一个键值,可注册多个变量的地址。
释放对象时,废弃谁都不持有的对象的同时,程序的动作是怎样的呢?下面我们来跟踪观察。对象将通过objc_release
函数释放。
- objc_release
- 因为引用计数为0 所以执行dealloc
- _objc_rootDealloc
- object_dispose
- objc_destructInstance
- objc_clear_deallocating
对象被废弃时最后调用的objc_clear_deallocating
函数的动作如下:
- 从weak 表中获取废弃对象的地址为键值的记录。
- 将包含在记录中的所有附有__weak 修饰符变量的地址,赋值为nil。
- 从weak 表中删除该记录。
- 从引用计数表中删除废弃对象的地址为键值的记录。
根据以上步骤,前面说的如果附有__weak
修饰符的变量所引用的对象被废弃,则将nil
赋值给该变量这一功能即被实现。由此可知,如果大量使用附有__weak
修饰符的变量,则会消耗相应的CPU 资源。良策是只在需要避免循环引用时使用__weak
修饰符。
使用__weak
修饰符时,以下源代码会引起编译器警告。
{
id __weak obj = [[NSObject alloc] init];
}
因为该源代码将自己生成并持有的对象赋值给附有__weak
修饰符的变量中,所以自己不能持有该对象,这时会被释放并被废弃,因此会引起编译器警告。
warning: assigning retained obj to weak variable; obj will be released after assignment [-Warc-unsafe-retained-assign]
id __weak obj = [[NSObject alloc] init];
^ ~~~~~~~~~~~~~~
编译器如何处理该源代码呢?
/* 编译器的模拟代码 */
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj, tmp);
objc_release(tmp);
objc_destroyWeak(&object);
虽然自己生成并持有的对象通过objc_initWeak
函数被赋值给附有__weak
修饰符的变量中,但编译器判断其没有持有者,故该对象立即通过objc_release
函数被释放和废弃。
这样一来,nil 就会被赋值给引用废弃对象的附有__weak
修饰符的变量中。下面通过NSLog
函数来验证一下。
{
id __weak obj = [[NSObject alloc] init];
NSLog(@"obj = %@", obj);
}
输出:
obj = (null)
立即释放对象
如前所述,以下源代码会引起编译器警告。
id __weak obj = [[NSObject alloc] init];
这是由于编译器判断生成并持有的对象不能继续持有。附有__unsafe_unretained 修饰符的变量又如何呢?
id __unsafe_unretained obj = [[NSObject alloc] init];
与__weak 修饰符完全相同,编译器判断生成并持有的对象不能继续持有,从而发出警告。
该源代码通过编译器转换为以下形式。/* 编译器的模拟代码 */ id obj = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); objc_release(obj);
objc_release 函数立即释放了生成并持有的对象,这样该对象的悬垂指针被赋值给变量obj 中。
那么如果最初不赋值变量又会如何呢?下面的源代码在ARC 无效时必定会发生内存泄漏。[[NSObject alloc] init];
由于源代码不使用返回值的对象,所以编译器发出警告。
可像下面这样通过向void 型转换来避免发生警告。(void)[[NSObject alloc] init];
不管是否转换为void,该源代码都会转换为以下形式
/* 编译器的模拟代码 */ id tmp = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(tmp, @selector(init)); objc_release(tmp);
虽然没有指定赋值变量,但与赋值给附有__unsafe_unretained 修饰符变量的源代码完全相同。由于不能继续持有生成并持有的对象,所以编译器生成了立即调用objc_release 函数的源代码。而由于ARC 的处理,这样的源代码也不会造成内存泄漏。
另外,能调用被立即释放的对象的实例方法吗?(void)[[[NSObject alloc] init] hash];
该源代码可变为如下形式:
/* 编译器的模拟代码 */ id tmp = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(tmp, @selector(init)); objc_msgSend(tmp, @selector(hash)); objc_release(tmp);
在调用了生成并持有对象的实例方法后,该对象被释放。看来“由编译器进行内存管理”这句话应该是正确的。
这次我们再用附有__weak
修饰符的变量来确认另一功能:使用附有__weak
修饰符的变量,即是使用注册到autoreleasepool
中的对象。
{
id __weak objobj1 = obj;
NSLog(@"%@", obj1);
}
该源代码可转换为如下形式:
/* 编译器的模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);
与被赋值时相比,在使用附有__weak
修饰符变量的情形下,增加了对objc_loadWeakRetained
函数和objc_autorelease
函数的调用。这些函数的动作如下。
objc_loadWeakRetained
函数取出附有__weak
修饰符变量所引用的对象并retain
。objc_autorelease
函数将对象注册到autoreleasepool
中。
由此可知,因为附有__weak
修饰符变量所引用的对象像这样被注册到autoreleasepool
中,所以在@autoreleasepool 块结束之前都可以放心使用。但是,如果大量地使用附有__weak
修饰符的变量,注册到autoreleasepool
的对象也会大量地增加,因此在使用附有__weak
修饰符的变量时,最好先暂时赋值给附有__strong
修饰符的变量后再使用。
比如,以下代码使用了5 次附有__weak
修饰符的变量o。
{
id __weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);
}
相应地,变量o 所赋值的对象也就注册到autoreleasepool
中5 次。
将附有__weak
修饰符的变量o 赋值给附有__strong
修饰符的变量后再使用可以避免此类问题。
{
id __weak o = obj;
id tmp = o;
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
NSLog(@"5 %@", tmp);
}
相应地,变量o 所赋值的对象也就注册到autoreleasepool
中5 次。
将附有__weak
修饰符的变量o
赋值给附有__strong
修饰符的变量后再使用可以避免此类问题。
{
id __weak o = obj;
id tmp = o;
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
NSLog(@"5 %@", tmp);
}
在“tmp = o;
”时对象仅登录到autoreleasepool
中1 次。
在iOS4 和OS X Snow Leopard 中是不能使用__weak
修饰符的,而有时在其他环境下也不能使用。实际上存在着不支持__weak
修饰符的类。
例如NSMachPort 类就是不支持__weak
修饰符的类。这些类重写了retain
/release
并实现该类独自的引用计数机制。但是赋值以及使用附有__weak
修饰符的变量都必须恰当地使用objc4
运行时库中的函数,因此独自实现引用计数机制的类大多不支持__weak
修饰符。
不支持__weak
修饰符的类,其类声明中附加了“__attribute__((objc_arc_weak_reference_unavailable))
”这一属性,同时定义了NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE
。如果将不支持__weak
声明类的对象赋值给附有__weak
修饰符的变量,那么一旦编译器检验出来就会报告编译错误。而且在Cocoa 框架类中,不支持__weak
修饰符的类极为罕见,因此没有必要太过担心。
allowsWeakReference/retainWeakReference 方法
实际上还有一种情况也不能使用
__weak
修饰符。
就是当allowsWeakReference
/retainWeakReference
实例方法(没有写入NSObject接口说明文档中)返回NO
的情况。这些方法的声明如下:- (BOOL)allowsWeakReference; - (BOOL)retainWeakReference;
在赋值给
__weak
修饰符的变量时,如果赋值对象的allowsWeakReference
方法返回NO,程序将异常终止。
即对于所有allowsWeakReference
方法返回NO 的类都绝对不能使用__weak
修饰符。
这样的类必定在其参考说明中有所记述。
另外,在使用__weak
修饰符的变量时,当被赋值对象的retainWeakReference
方法返回NO 的情况下,该变量将使用“nil”。如以下的源代码:{ id __strong obj = [[NSObjectalloc] init]; id __weak o = obj; NSLog(@"1 %@", o); NSLog(@"2 %@", o); NSLog(@"3 %@", o); NSLog(@"4 %@", o); NSLog(@"5 %@", o); }
由于最开始生成并持有的对象为附有
__strong
修饰符变量obj 所持有的强引用,所以在该变量作用域结束之前都始终存在。因此如下所示,在变量作用域结束之前,可以持续使用附有__weak
修饰符的变量o 所引用的对象。1 <NSObject: 0x753e180> 2 <NSObject: 0x753e180> 3 <NSObject: 0x753e180> 4 <NSObject: 0x753e180> 5 <NSObject: 0x753e180>
下面对
retainWeakReference
方法进行试验。我们做一个MyObject类,让其继承 NSObject类并实现retainWeakReference
方法。@interfaceMyObject :NSObject { NSUInteger count; } @end @implementationMyObject - (id)init { self = [super init]; return self; } - (BOOL)retainWeakReference { if(++count > 3) return NO; return [super retainWeakReference]; } @end
该例中,当
retainWeakReference
方法被调用4次或4次以上时返回NO。在之前的源代码中,将从NSObject类生成并持有对象的部分更改为MyObject类。{ id strong obj = [[Myobject alloc] init]; id weak o = obj; NSLog(@"1 %@",o); NSLog(@"2 %@",o); NSLog(@"3 %@”,o); NSLog(@"4 %@",o); NSLog(@"5 %@",o); }
以下为执行结果。
1 <MyObject:0x753e180> 2 <MyObject:0x753e180> 3 <MyObject:0x753e180> 4 (nul1) 5 (null)
从第4次起,使用附有
__weak
修饰符的变量o时,由于所引用对象的retainWeakReference
方法返回NO,所以无法获取对象。像这样的类也必定在其参考说明中有所记述。
另外,运行时库为了操作__weak
修饰符在执行过程中调用allowsWeakReferencel
/retainWeakReference
方法,因此从该方法中再次操作运行时库时,其操作内容会永久等待。原本这些方法并没有记入文档,因此应用程序编程人员不可能实现该方法群,但如果因某些原因而不得不实现,那么还是在全部理解的基础上实现比较好。
(三)__autoreleasing 修饰符
将对象赋值给附有__autoreleasing
修饰符的变量等同于ARC 无效时调用对象的autorelease
方法。我们通过以下源代码来看一下。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
该源代码主要将NSObject 类对象注册到autoreleasepool
中,可作如下变换:
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
这与苹果的autorelease
实现中的说明完全相同。虽然ARC 有效和无效时,其在源代码上的表现有所不同,但autorelease
的功能完全一样。
在alloc
/new
/copy
/mutableCopy
方法群之外的方法中使用注册到autoreleasepool
中的对象会如何呢?下面我们来看看NSMutableArray
类的array
类方法。
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
这与前面的源代码有何不同呢?
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
虽然持有对象的方法从alloc
方法变为objc_retainAutoreleasedReturnValue
函数, 但注册autoreleasepool
的方法没有改变,仍是objc_autorelease
函数。
(四)引用计数
这里提供获取引用计数数值的函数。
uintptr_t _objc_rootRetainCount(id obj)
如上声明的_objc_rootRetainCount
函数可获取指定对象的引用计数数值。请看以下几个例子。
id __strong obj = [[NSObject alloc] init];
NSLog(@"retain count =8d", _objc_rootRetainCount(obj));
该源代码中,对象仅通过变量obi的强引用被持有,所以为1。
retain count = 1
下面使用__weak
修饰符。
{
id __strong obj = [[NSObject alloc] init];
id __weak o = obj;
NSLog(@"retain count =%d", _objc_rootRetainCount(obj));
}
由于弱引用并不持有对象,所以赋值给附有__weak
修饰符的变量中也必定不会改变引用计数数值。
retain count = 1
结果同预想一样。那么通过__autoreleasing
修饰符向autoreleasepool
注册又会如何呢?
@autoreleasepool {
id __strong obj = [NSObject alloc] init];
id __autoreleasing o = obj:
NSLog(@"retain count *d", objc_rootRetainCount(obj));
}
结果如下:
retain count = 2
对象被附有__strong
修饰符变量的强引用所持有,且被注册到autoreleasepool
中,所以为2。以下确认@autoreleasepool 块结束时释放已注册的对象。
{
id __strong obj = [[NSObject alloc] init];
@autoreleasepool {
id __autoreleasing o = obj;
NSLog(@"retain count = %d in @autoreleasepool", _objc_rootRetainCount(obj));
}
NSLog(@"retain count = %d", _objc_rootRetainCount(obj));
}
在@autoreleasepool 块之后也显示引用计数数值。
retain count = 2 in @autoreleasepool
retain count = 1
如我们预期的一样,对象被释放。
以下在通过附有__weak
修饰符的变量使用对象时,基于显示autoreleasepool
状态的_objc_autoreleasePoolPrint
函数来观察注册到autoreleasepool
中的引用对象。
@autoreleasepool {
id __strong obj = [[NSObject alloc] init];
_objc_autoreleasePoolPrint();
id __weak o = obj;
NSLog(@"before usingweak: retain count = %d", _objc_rootRetainCount(obj));
NSLog(@"class = %@", [o class]);
NSLog(@"after using__weak: retain count = %d", _objc_rootRetainCount (obj));
_objc_autoreleasePoolPrint();
}
通过以上过程我们可以看出,不使用autoreleasing
修饰符,仅使用附有weak声明的变量也能将引用对象注册到了 autoreleasepool
中。
虽然以上这些例子均使用了_objc_rootRetainCount
函数,但实际上并不能够完全信任该函数取得的数值。对于已释放的对象以及不正确的对象地址,有时也返回“1”。另外,在多线程中使用对象的引用计数数值,因为存有竞态条件的问题,所以取得的数值不一定完全可信。
虽然在调试中_objc_rootRetainCount
函数很有用,但最好在了解其所具有的问题的基础上来使用。