写在翻译之前:当初看到这本书的时候,感觉深入浅出讲得比较到位,但是在市面上看到的翻译版本翻译的却没有原著的精髓和味道。所以产生了自己将其翻译一下给初学者一些便利的想法。所以才有了这个系列的第一章的翻译。目前剩余的部分依然在翻译过程中,估计不久之后就可以陆续地发出了。
因为本人的水平或者用词问题,本翻译难免有不周详或不正确之处。如果有人看到还望指出,我一定会尽力地修改那些不正确的部分,让更多的人可以看到更优质的资料。
Chapter 1
Life before Automatic Reference Counting
OSX 和 iOS5 提供了一种新的内存管理机制-- 自动引用计数(ARC)。简而言之,ARC使用编译器来对内存进行管理,而程序员则不需要参与,从而显著地提高了程序的性能。
在第二章和第三章,您将会看到ARC有多么的强大。但是在进入这个美好的世界之前,我们最好还是先回顾一下在没有ARC的时候内存是如何进行管理的。通过这样做,您将会构建出更精确的ARC知识体系,并且通过以后两章所讲述的知识您可以构建出一个更加强壮的ARC工程。
我们将从内存管理及其概念的概览开始,随后会说明其特征 - 诸如alloc dealloc autorelease - 等的实现。
引用计数内存管理概览
在大多数Objective-C编程的情况下,我们都可以把“内存管理”理解为“引用计数”。内存管理意味着开发人员需要在程序需要的时候开辟一片内存,并且在程序不再需要这片内存时将其释放掉。如果那些不再被需要的内存块没有被正确地释放掉的话,就是对于宝贵资源的一种浪费了,甚至会引起应用的崩溃。在1960年被George E. Collins发明的“引用计数方法”,使得内存管理变得更加容易。
为了更加形象地说明引用计数,我们将以如下这个办公室的光源管理的类比方式进行演示。(如图1-1)
图1-1
让我们想象一下,办公室里只有一盏灯。在早上,当有人来到办公室后他就会打开这盏灯。当他离开办公室的时候,因为他不再需要这盏灯了所以他就把它关上了。如果当这个办公室中有很多人时并且每个人都会在进出办公室时打开或关闭这盏灯会怎么样呢?当他离开时,他就关掉了这盏灯,导致其他人不得不在黑暗中办公了。(OOps!)(如图1-2)
图1-2
为了解决这个问题,我们需要一套规则去保证当屋里有一个人或更多的人时灯是开着的,且当所有人都离开后灯被关掉。
1.当有人进入办公室时,他需要打开这盏灯。
2.当其他人随后进入时,他使用这盏已经打开的灯。
3.当有人离开时,他不再需要这盏灯。
4.当最后一个人离开时,他需要关闭这盏灯。
为了实现以上规则,我们引入一个计数来获取房间中到底有多少人,让我们来看看它是如何工作的。
1.当有人进入一个空的办公室时,计数+1.它从0变成1.所以灯亮了。
2.当另一个人进入时,计数+1;它从1变成2.
3.当有人离开时,计数-1,它从2变成1.
4.当最后一个人离开时,计数变成0,所以灯灭掉。
如图1-3所示,通过计数方法我们可以准确地控制这盏灯。这盏灯只在所有人都离开时才会关掉。
图1-3
让我们看看这个比喻是如何帮助我们理解内存管理的。在Objective-C中,这盏灯相当于一个对象。虽然这个办公室仅有一盏灯,但是在Objective-C中,我们可以在电脑资源的范围下,拥有很多的对象。
每个人就相当于Objective-C中的一段内容。内容可以认为是一段代码,一个变量,一个变量作用域。它意味着有些事情正在作用于这个对象。表格1-1说明了办公室中的灯和Objective-C中的对象的关系。
表格1-1
灯的动作 | Objective-C中对象的动作 |
---|---|
打开 | 创建一个对象并且持有它 |
使用它 | 持有这个对象 |
不使用它 | 释放对这个对象的持有权 |
关灯 | 释放这个对象 |
既然我们可以使用计数管理灯,那么我们也可以使用这种方式来管理内存。换句话说,我们可以通过引用计数来管理Objective-C中的对象。如图1-4所示
图1-4
在这张图中描述了使用引用计数进行内存管理的概念。在随后的章节中,我们将深挖这些概念,并且给出一些例子来更好的说明。
更深层次地探究内存管理
使用引用计数,您也许会认为您需要记住一个对象自身的引用计数数值和哪些内容正在使用一个对象等等。但是事实上您并不需要这么做。相反,你应该记住引用计数的如下规则:
* 你拥有你所创建的任何对象。
* 你可以通过retain操作拥有一个对象。
* 当不在需要一个对象时,你必须要放弃一个对象的拥有权。
* 你一定不要放弃一个您不拥有的对象的所有权。
以上就是引用计数的全部规则。你需要做的所有事情就是遵守这些规则。只要遵守以上规则,您就不需要再操心引用计数的事情了。
规则中的专用名词 - “创建”, “获取拥有权”, “放弃拥有权” 和 “销毁” 是在引用计数中经常使用的词语。在表格1-2中,展示了这些名词所对应的在Objective-C中的方法。
表格1-2
Objective-C中的动作 | Objective-C的方法 |
---|---|
创建并拥有一个对象 | alloc / new / copy / mutableCopy group |
拥有一个对象 | retain |
释放一个对象 | release |
销毁一个对象 | dealloc |
基本上,你alloc一个对象,再在某些时刻retain它。然后再把那些你alloc / retain的对象release掉。Dealloc方法在这个对象从内存中被移除的时候被调用。
注意:如果你使用了一次alloc又使用了一次retain,那么你需要调用release两次.
这些方法并不是Objective-C提供的,而是由Cocoa Framework的一部分 - Foundation Framework所提供。在Foundation Frame中,NSObject 有一个类方法alloc, 和实例方法retain, release 和 dealloc 来进行内存管理。(如图1-5所示)。如何使用它们来完成内存的管理会在随后的章节 “实现 alloc , retain, release 和 dealloc”中介绍。
图1-5
下面让我们来逐条学习这些规则。
你拥有你所创建的任何对象
当你使用了任何一个以如下单词开头的方法的时候,就意味着你创建了一个对象并拥有它。 * alloc * new * copy * mutableCopy
让我们看看如何通过例子源代码来创建一个对象。如下的例子使用了 alloc 方法来创建并拥有一个对象。
/*
* You create an object and have ownership. */
id obj = [[NSObject alloc] init];
/*
* Now, you have ownership of the object. */
通过调用NSObject的类方法,你创建并持有了它。这个变量obj有一个指针指向被创建的对象。你也可以通过new方法进行创建。[NSObject new]和[[NSObject alloc] init]做的事情几乎是一样的。
/*
* You create an object and have ownership. */
id obj = [NSObject new];
/*
* Now you have ownership of the object. */
NSObject 的实例方法"copy"创建一份对于一个对象的拷贝。这个类必须正确地要实现NSCopying代理和copyWithZone:方法。同样的,NSObject实例方法"mutableCopy"创建一个对象的可以修改的拷贝,这个类必须要正确地实现NSMutableCopying代理和mutableCopyWithZone:方法。mutableCopy和copy 方法的区别和 NSArray 和 NSMutableArray 是相似的。这些方法都和 new 方法一样创建一个新对象出来。因此,你拥有这个对象。
通过前面的描述,当你使用一个以alloc, new, copy 或 mutableCopy 开头的方法时,你创建并拥有一个对象。如下是一些方法的例子。
* allocMyObject
* newThatObject
* copyThis
* *mutableCopyYourObject
但是,名称的单词形变并不适用这套规则。
* allocate
* newer
* copying
* mutableCopyed
注意:请使用骆驼命名法。骆驼命名法是一种使用单词首字母大写的命名方式。了解更多关于骆驼命名法的信息,请方位:http://en.wikipedia.org/wiki/CamelCase
你可以通过retain方法来持有一个对象
有时并不是只有 alloc/new/copy/mutableCopy 方法组才返回一个对象。在这个情况下,你并没有创建一个对象,所以你也并不拥有它。如下的例子就是NSMutableArray的类方法 array.
/*
* Obtain an object without creating it yourself or having ownership */
id obj = [NSMutableArray array];
/*
* The obtained object exists and you don’t have ownership of it. */
变量obj 有一个对NSMutableArray对象的引用,但是你并不持有它。为了持有它,你需要使用retain方法。
/*
* Obtain an object without creating it yourself or having ownership */
id obj = [NSMutableArray array];
/*
* The obtained object exists and you don’t have ownership of it. */
[obj retain];
/*
* Now you have ownership of it. */
在调用 retain 方法后,你持有了这个对象就好像你通过调用 alloc/new/copy/mutableCopy 方法来获取了一个对象一样。
当你不再需要一个对象时,你必须要释放一个对象的所有权
当你持有一个对象但是你并不再需要这个对象了,你必须要通过release方法来释放这个对象。
/*
* You create an object and have ownership. */
id obj = [[NSObject alloc] init];
/*
* Now you have ownership of the object. */
[obj release];
/*
* The object is relinquished. *
* Though the variable obj has the pointer to the object, * you can’t access the object anymore.
*/
在上面的例子中,在一个对象通过alloc方法创建并被持有后,你通过release方法释放了它。你可以通过如下方法对被持有的对象做相同的事。
释放一个被持有的对象
/*
* Obtain an object without creating it yourself or having ownership */
id obj = [NSMutableArray array];
/*
* The obtained object exists and you don’t have ownership of it. */
[obj retain];
/*
* Now you have ownership of the object. */
[obj release];
/*
* The object is relinquished.
* You can’t access the object anymore. */
在这些情况下,你必须要通过release方法来对你所持有的对象释放所有权:你通过 alloc/new/copy/mutableCopy 方法来创建并持有一个对象时,或你通过retain方法持有了一个对象的时候。
下面,让我们来看看如何返回一个被创建的对象。
如下的示例代码展示了如何通过一个方法来创建一个对象。
- (id)allocObject
{
/*
* You create an object and have ownership. */
id obj = [[NSObject alloc] init];
/*
* At this moment, this method has ownership of the object. */
return obj; }
如果一个方法返回一个此方法所持有的对象, 这种持有关系就被传递给了调用这个方法的人。同样的,请注意为了和 alloc/new/copy/mutableCopy保持一致,这个方法必须要以alloc命名。
/*
* Obtain an object without creating it yourself or having ownership */
id obj1 = [obj0 allocObject];
/*
* Now you have ownership of the object. */
你调用了allocObject方法,这就意味着你创建并且拥有了一个对象,因为这个方法是以alloc开头的。
下面,我们来看看如何实现一个类似于[NSMutableArray array]的方法。
返回一个没有持有关系的对象
[NSMutableArray array]方法返回一个并不被调用者所持有的对象。让我们看看该如何实现该类型的方法。
我们不能声明一个以 alloc/new/copy/mutableCopy 开头的方法。在下面的例子中,我们使用"object"来作为方法名。
- (id)object
{
id obj = [[NSObject alloc] init];
/*
* At this moment, this method has ownership of the object. */
[obj autorelease];
/*
* The object exists, and you don’t have ownership of it. */
return obj; }
为了实现这样的方法,我们使用了 autorelease 方法(如图1-6)。通过调用 autorelease , 你可以创建一个对象但是并不持有它。Autorelease 提供一个当对象的生存周期结束后正确地释放对象的机制。
图1-6
举个例子,NSMutableArray的类方法array 就是类似这样被实现的。请注意我们依旧按照惯例这个方法并不以 alloc/new/copy/mutableCopy 命名。
id obj1 = [obj0 object];
/*
* The obtained object exists and you don’t have ownership of it. */
你可以通过retain方法来持有这个被autorelease的对象。
id obj1 = [obj0 object]; /*
* The obtained object exists and you don’t have ownership of it. */
[obj1 retain];
/*
* Now you have ownership of the object. */
我将会在后面的章节中对autorelease方法做出更多的解释。
你一定不能释放你并不持有的对象的所有权
通过前面的描述,当你持有一些对象时,你一定要通过调用release方法来释放它们。但是,当你并不持有这些对象时,你一定不可以调用release方法来释放一个对象。如果你这么做了,这个应用将会崩溃。例如,在释放了你所持有的对象后,如果你再去release它,那么应用将会崩溃。
/*
* You create an object and have ownership. */
id obj = [[NSObject alloc] init];
/*
* Now you have ownership of the object. */
[obj release];
/*
* Object is relinquished. */
[obj release];
/*
* You relinquished the object, of which you don’t have ownership!
* The application will crash!
*
* The applications will crash in these cases:
* When you call the release method to an already-disposed-of object. * When you access an already-disposed-of object.
*/
同样,下面的例子你也释放了一个你并不持有的对象。
id obj1 = [obj0 object];
/*
* The obtained object exists and you don’t have ownership of it. */
[obj1 release];
/*
* You relinquished the object of which you don’t have ownership! * The application will crash sooner or later.
*/
在这些例子中,你一定不可以释放那些你并不持有的对象。否则就会引起应用崩溃。
我们已经学习了这四条规则,以这四条规则进行引用计数的内存管理。下面,我们要学习如何实现 alloc, retain, release和 dealloc 然后它们是如何工作的。
实现alloc, retain, release 和 dealloc
在很多的OSX和iOS的已经开源的源代码中,如上面所提到的,alloc, retain , release和 dealloc都是作为Cocoa Frameworkd的一部分的Foundation Framework中NSObject的方法。幸运的是,因为Core Foundation Framework 是苹果开源代码的一部分,关于内存管理部分的代码是公开的。但是,关于这部分代码的实现却并不是公开的,所以我们无法得到内存管理的全景图。所以我们来使用替代的代码GNUstep来看看。
GNUstep是一个和Cocoa Framework兼容的实现。虽然我们不能完全地明白苹果的实现,但是工作的方式和实现的方法一定是类似的。了解GNUstep的源代码帮助我们猜测苹果的Cocoa 实现。
alloc 方法
让我们从GNUstep中NSObject类的alloc方法开始吧。首先注意,这本书中的有些源代码也许没有改写为使得关键部分清晰的样子。
"alloc"方法被如下调用:
id obj = [NSObject alloc];
关于NSObject.m中alloc方法的实现如下清单1-1所示:
+ (id) alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+ (id) allocWithZone: (NSZone*)z
{
return NSAllocateObject (self, 0, z);
}
清单1-1
在allocWithZone: 方法中,一个对象被NSAllocateObject函数来分配。该函数的实现如清单1-2所示:
清单 1–2. GNUstep/Modules/Core/Base/Source/NSObject.m NSAllocateObject
struct obj_layout {
NSUInteger retained; };
inline id
NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
int size = /* needed size to store the object */ id new = NSZoneMalloc(zone, size);
memset(new, 0, size);
new = (id)&((struct obj_layout *)new)[1];
}
NSAllocateObject方法调用NSZoneMalloc来分配内存区域。在此之后,这个区域就被0所填充,并且返回这篇内存的指针。
注意:一开始,NSZone是为了防止内存碎片化(如图1-7)。通过不断地改变zone,内存分配可以越来越高效。 但是如今,Objective-C运行时忽略zone就像是你在"改变为ARC 释放笔记"所看到的。因为最近运行时内存管理算法已经足够高效,通过使用zone因为其复杂性已经并不是一个最优化的选择了。
图1-7
通过去掉那部分和NSZone有关的代码,alloc方法可以简写为:
struct obj_layout {
NSUInteger retained; };
+ (id) alloc
{
int size = sizeof(struct obj_layout) + size_of_the_object; struct obj_layout *p = (struct obj_layout *)calloc(1, size); return (id)(p + 1);
}
现在你知道alloc是如何工作的了,让我们来看看retain!
retain 方法
alloc方法返回一个由0填充的,并且由一个obj_layout作为头的内存区域,其中头部包含一个叫“retained”的变量用来储存引用计数。如图1-8所示,就是一个对象在GNUstep下所使用的结构。
图1-8
你可以通过retainCount方法获取一个对象的引用计数。
id obj = [[NSObject alloc] init]; NSLog(@"retainCount=%d", [obj retainCount]);
/*
* retainCount=1 is displayed. */
就在alloc方法被调用后,引用计数为1.下一段代码展示了在GNUstep中,retainCount方法的实现。
清单 1–4. GNUstep/Modules/Core/Base/Source/NSObject.m retainCount ~~~ - (NSUInteger) retainCount { return NSExtraRefCount(self) + 1; } inline NSUInteger NSExtraRefCount(id anObject) { return ((struct obj_layout *)anObject)[-1].retained; } ~~~
源代码通过对象指针搜索了对象头文件,获取了引用计数的值(图1-9)。
图1-9
因为这个代码块在被分配的时候被0所填充,所以"retained"的值为0.所以retainCount方法通过“NSExtraRefCount(self) + 1”来返回1.我们可以猜测 "retain" 和 “release” 方法也会通过+1和-1来改变获取到得值。
[obj retain];
让我们在清单1-5中看一下retain方法的实现。 Listing 1–5. GNUstep/Modules/Core/Base/Source/NSObject.m 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. 下面,让我们来学习 "release" 方法,这个和"retained" 方法相反的方法。
release 方法
我们可以简单地猜测 release 方法将会有 "retain --"。同时,当变量为0的时候也会有相应的处理代码。
[obj release];
release方法的实现如清单1-6所示。 清单 1–6. GNUstep/Modules/Core/Base/Source/NSObject.m release
- (void) release
{
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc]; }
BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_layout *)anObject)[-1].retained--;
return NO; }
}
根据我们的猜想,"retained"是被减去1. 如果这个值是0的时候,这个对象会通过dealloc被移除。让我们看看dealloc是如何实现的。
dealloc 方法
清单1-7是dealloc方法的实现。 清单 1–7. GNUstep/Modules/Core/Base/Source/NSObject.m dealloc
- (void) dealloc
{
NSDeallocateObject (self);
}
inline void NSDeallocateObject(id anObject)
{
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
free(o); }
这就是如何来对内存块进行移除的方法。
我们已经看到了在GNUstep中对于alloc, retain, release 和 dealloc方法的实现,同时也学习了以下知识:
* 所有的Objective-C对象都有一个整形值叫引用计数。
* 引用计数在对象调用了alloc/new/copy/mutableCopy或 retain 方法后会加1
* 引用计数会在调用release方法后减1
* dealloc方法会在引用计数为0时被调用
下面,我们来看看在苹果的实现。
苹果对于alloc, retain, release 和 dealloc方法的实现
如前所述,NSObject类的代码并不是开源的。我推荐使用XCode的bug查找器(lldb)来观察其如何工作。首先,我们需要在NSObject的alloc方法中设置一个断点,来观察在lldb中发生了什么。如下是在alloc函数中所有被调用到的方法。
+alloc
+allocWithZone:
class_createInstance
calloc
NSObject 类方法 alloc 调用 allocWithZone: 然后调用 classcreateInstance 方法,该方法在 Objective-C 的运行时相关文档中有介绍。最后通过 calloc 方法来分配一个内存块。以上过程看起来和GUNstep的实现并无太大差异。我们可以在objc4库中的 runtime/objc-runtime- new.mm 文件中查看 classcreateInstance的源代码。
那么NSObject 的其他实例方法诸如 retainCount, retain 和 release 是如何实现的呢?下面就为您列举出来。
retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(同样地,dealloc 也会在 CFBasicHashRemoveValue返回值为0时被调用)
在上面所有的方法中,__CFDoExternRefOperation 都被调用了。然后这个方法会调用具有相似名称的方法。这些方法都是共有的。如您所见,如果一个方法以CF开头,那么您就可以在Core Foundation Framework中找到它的源代码。 清单1-8 简要地说明了 __CFDoExternRefOperation 方法在 CFRuntime.c 中的实现。
清单 1–8. CF/CFRuntime.c __CFDoExternRefOperation
int __CFDoExternRefOperation(uintptr_t op, id obj) { CFBasicHashRef table = get hashtable from 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 方法。我们可以猜想这些方法将会以以下方式实现。
- (NSUInteger) retainCount
{
return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount, self);
}
- (id) retain
{
return (id)__CFDoExternRefOperation(OPERATION_retain, self);
}
- (void) release
{
return __CFDoExternRefOperation(OPERATION_release, self);
}
如你所见,__CFDoExternRefOperation 方法在苹果中的实现似乎是使用了一个哈希表,如图(1-10)所示.
在GNUstep的实现中,引用计数被储存在每个对象在内存中的头文件中。但是在苹果的实现中,所有对象的的引用计数都被存储在一张哈希表中。虽然GNUstep的实现看起来更加简洁和高效,但是苹果也有其自身的优点。
像GNUstep那样将引用计数存储在对象头文件中,有如下优点:
* 更少的代码
* 因为每个对象的引用计数都被存放在该对象在内存中的内存块的头文件中,所以对于对象生命周期的管理将会变得很容易
但是对于苹果的哈希表储存,又有什么优点呢?
* 因为每个对象都没有头文件,所以不必要考虑头文件中的格式问题;
* 通过遍历哈希表,所有对象的内存块都被访问到。
其中第二点对于debug来讲是及其有用的。当一些对象的内存块被破坏后,哈希表还是依然存在的。如图(1-11)所示,debugger依然可以访问到一些对象的指针。
同样地,检测内存泄露,instruments检测这些哈希表并确定是否有人正在持有这些对象。
这就是苹果的实现。现在让我们来好好看看苹果究竟是如何做这些事情的,并且在苹果中我们还有一个要学习的点:autorelease!
Autorelease
因为它的名称,你也许会认为autorelease是一种和ARC差不多的东西。但是实际上并不是这样。它更像是C语言中的"自动变量"。
那么让我们从C语言的自动变量开始吧。然后我们通过GNUstep来卡看autorelease是如何实现的,最后我们再看看苹果对于该部分的实现。
自动变量
自动变量是一个词意上的区域变量,当区域结束后,这个变量会被自动移除。
{
int a;
}
/*
* Because the variable scope is left,
* auto variable ‘int a‘ is disposed of and can‘t be accessed anymore. */
通过autorelease,你可以让对象和自动变量有一样的行为。这也就意味着当执行某块代码时,release方法会被对象自动调用。你也可以控制那个代码块本身。
以下的步骤和图1-12为您展示使用了autorelease方法的对象。
1.创建一个NSAutoreleasePool 对象 2.让已经被分配的对象调用autorelease方法; 3.废弃 NSAutoreleasePool 对象
一个在创建和销毁之间的NSAutoreleasePool对象就等同于一个C中的变量域。当一个NSAutoreleasePool对象被废弃时,所有调用过autorelease方法的对象都会自动调用release方法。下面是一些示例代码。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
在上面的最后一行代码中,[pool drain]会做[pool release]所做的事情。
在Cocoa Framework中,NSAutoreleasePool对象一直在被创建,持有或者废弃,就像是NSRunLoop对象一样,NSRunLoop也是应用的主回路。(如图1-13所示)所以事实上你并不需要显示地直接使用NSAutoreleasePool对象。 图1-13
但是当有太多的autorelease对象时,应用的内存将会变得非常紧张。(如图1-14)因为当NSAutoreleasePool对象被释放时这些对象依然存在。一个典型的例子是加载并改变图片的大小。很多的autorelease对象,比如用来读取文件的NSData对象,UIImage对象的数据和被改变大小后的图片数据是同时存在的。
for (int i = 0; i < numberOfImages; ++i) {
/*
* Processing images, such as loading,etc.
* Too many autoreleased objects exist,
* because NSAutoreleasePool object is not discarded. * At some point, it causes memory shortage.
*/
}
在这种情况下,你应该在合适的时间自己显示地创建并废弃一个NSAutoreleasePool对象。(如图1-15)
for (int i = 0; i < numberOfImages; ++i) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/*
* Loading images, etc.
* Too many autoreleased objects exist. */
[pool drain];
/*
* All the autoreleased objects are released by [pool drain]. */
}
使用Cocoa Framework,你可以看见很多的类方法返回autorelease的对象。比如返回NSMutableArray的 arrayWithCapacity: 方法。
id array = [NSMutableArray arrayWithCapacity:1];
上面的代码和下面的代码是等同的:
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
autorelease 的实现
在这个小节中,我们将讨论在GUNstep中autorelease的实现.
[obj autorelease];
这个代码调用了NSObject的实例方法 autorelease. 在清单1-9中展示了autorelease方法的实现
清单 1–9. GNUstep/Modules/Core/Base/Source/NSObject.m autorelease
- (id) autorelease
{
[NSAutoreleasePool addObject:self];
}
事实上,autorelease调用了NSAutoreleasePool的类方法 addObject. 在GUNstep中,它的实现稍有不同。但是下面的是优化过的实现。
在Objective-C中的优化
在GNUstep中,autorelease方法被以一种不正常的方式实现,为了实现更优化的目的。因为这个方法在iOS和OSX中被大量地调用。这是一种叫做IMP的特殊机制。当这个框架被初始化后,它缓存一些结果,比如函数指针和类和方法的名称优化。如果这个机制不存在的话,这些过程就不得不在autorelease被调用时完成。
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缓存。它可以被下面这种没有IMP缓存机制的方法重写。根据环境的不同,它可能会比没有IMP缓存机制的方法快两倍。
- (id) autorelease
{
[NSAutoreleasePool addObject:self];
}
让我们看看是如何实现NSAutoreleasePool类的。在清单1-10中,简单地阐述了NSAutoreleasePool的代码。
清单 1–10. GNUstep/Modules/Core/Base/Source/NSAutoreleasePool.m addObject
+ (void) addObject: (id)anObj
{
NSAutoreleasePool *pool = getting active NSAutoreleasePool; if (pool != nil) {
[pool addObject:anObj]; } else {
NSLog(@"autorelease is called without active NSAutoreleasePool."); }
}
类方法addObject 调用了在正在活动的 NSAutoreleasePool对象的实例方法 addObject 。在下一个例子中,一个变量 pool 是正在活动的 NSAutoreleasePool对象,
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init];
[obj autorelease];
当多个NSAutoreleasePool对象存在并被嵌套时,最里层的对象是活动的。在下一个例子中,pool2 是正在活动的。
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的实现。(清单 1-11)
- (void) addObject: (id)anObj
{
[array addObject:anObj];
}
它将一个对象加入到一个可变数组之中。在原始的GNUstep实现中,链表被用来替代数组。但是,这个对象被放入一个容器中,也就意味着当这个对象调用实例方法autorelease时,这个对象被加入到了正在活动的NSAutoreleasePool对象的容器之中。
[pool drain];
下面,让我们看看正在活动的NSAutoreleasePool对象嗲用drain方法时是如何被移除的。(清单1-12)
清单 1–12. GNUstep/Modules/Core/Base/Source/NSAutoreleasePool.m drain
- (void) drain
{
[self dealloc];
}
- (void) dealloc
{
[self emptyPool];
[array release]; }
- (void) emptyPool
{
for (id obj in array) {
[obj release]; }
}
我们可以看到所有在这个pool中的对象都被调用了release方法。
苹果对于 autorelease 的实现
我们可以看到苹果对于autorelease的实现是放在 runtime/obje-arr.mm 文件中的,这个文件位于 objc4 库中。源代码如清单1-13所示.
清单 1–13. objc4/runtime/objc-arr.mm class AutoreleasePoolPage
class AutoreleasePoolPage
{
static inline void *push()
{
/* It corresponds to creation and ownership of an NSAutoreleasePool object */
}
static inline void pop(void *token)
{
/* It corresponds to disposal of an NSAutoreleasePool object */
releaseAll(); }
static inline id autorelease(id obj)
{
/* It corresponds to NSAutoreleasePool class method addObject. */ AutoreleasePoolPage *autoreleasePoolPage = /* getting active AutoreleasePoolPage
object */
autoreleasePoolPage->add(obj); }
id *add(id obj)
{
/* add the obj to an internal array; */
}
void releaseAll()
{
/* calls release for all the objects in the internal array */
} };
这些方法和AutoreleasePoolPage类被使用C++的类和动态数组来实现。方法看起来和GNUstep的工作方法是一致的。我们已经在前面用了debugger,我们发现这些方法都被在autorelease和NSAutoreleasePool的类方法中调用。这些方法将调用objc4中和autorelease相关的函数。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; /* equivalent to objc_autoreleasePoolPush() */
id obj = [[NSObject alloc] init];
[obj autorelease];
/* equivalent to objc_autorelease(obj) */
[pool drain];
/* equivalent to objc_autoreleasePoolPop(pool) */
顺便,在iOS中,NSAutoreleasePool中有一个类方法来检测所有的autorelease对象的状态。showPools方法把所有NSAutoreleasePool的状态打印到控制台。但是它只供调试或debug使用,因为它是一个私有的方法。你可以以如下方式使用:
[NSAutoreleasePool showPools];
根据最新的Objective-C运行时,有一个替代showPools的方法。因为showPools方法仅仅在iOS上适用,所以有了objcautoreleasePoolPrint()方法。这个方法同样也是私有方法,只能在debug的时候使用。
/* declare function */
extern void _objc_autoreleasePoolPrint();
/* display autoreleasepool status for debug. */ _objc_autoreleasePoolPrint();
然后你就会看到相应的AutoreleasePoolPage状态了。
自动释放的NSAutoreleasePoll对象
问题:如果一个NSAutoreleasePool对象调用了autorelease方法会发生什么呢?
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [pool autorelease];
答案:这个程序将会崩溃。
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException‘ reason: ‘*** -[NSAutoreleasePool autorelease]:
Cannot autorelease an autorelease pool‘
当autorelease被Objective-C的Foundation Framework调用的时候,大多数是调用一个NSObject的实例方法。但是NSAutorelease类重写了autorelease方法来展示一个错误,使得autoreleasepool在调用了autoreleasePool后展示一个错误。
总结
在这一章中,您学习了如下知识:
* 内存管理中引用计数的概念;
* alloc, retain, release 和 dealloc 方法是如何被实现的;
* autorelease的机制和它实现的方法;
这些项在ARC诞生后依然是很重要的。在下一章中,我们将学习情形将会如何变化。