[OC学习笔记]自动引用计数
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]自动引用计数相关的知识,希望对你有一定的参考价值。
一、什么是自动引用计数
自动引用计数(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];
[OC学习笔记]自动引用计数