ARC以前的故事
Posted 笑这荒唐世界
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ARC以前的故事相关的知识,希望对你有一定的参考价值。
苹果是在OSX狮子和ios5之后才提供的ARC,全称叫自动内存管理.
ARC:
- 让编译器来替代程序猿进行内存管理.
- 这极大的提高了效率.
-
本章先看看什么是MRC—ARC出来之前的内存管理世界—手动内存管理.
引用计数
-解释下,这个”引用”,大体意思应该类似于指针”指向”的意思.
以教室内的灯举例子:
1. 当第一个人进入教室的时候,计数+1,引用计数从0变为1,开灯
2. 当另一人进入教室的时候,+1,引用计数从1变为2.
3. 当某人从教室离开时,计数-1,引用计数从2变为1.
4. 当最后一人离开时,计数变为0,关灯.
运用到内存管理上,上文中的每个人可以看做是一个OC上下文.
上下文:这个词用来表示一段程序代码,一个变量,一个变量范围,或一个对象.表示对一个目标对象的处理.
进一步探索内存管理
不需要记忆引用计数的数目,牢记下面规则:
- 你拥有你创建的对象的所有权 注:以后直接用”拥有”表示”拥有…的所有权”
- 用retain来拥有一个对象的所有权
- 当不再需要时,必须放弃你拥有的对象的所有权
- 无权放弃你不拥有的对象的所有权
对象操作 | OC方法 |
---|---|
创建并拥有 | alloc/new/copy/mutableCopy(开头的一类方法) |
拥有 | retain |
放弃 | release |
销毁 | dealloc |
这些方法不是OC语言的,而是Cocoa框架中的Foundation框架里的一部分.在Foundation框架中,NSObject类有一个类方法alloc,以及实例方法retain,release,以及dealloc来处理内存管理.
你拥有你创建的对象
你如果用以以下前缀开头的方法,就会创建并拥有一个对象 注:文中的意思可能是,系统框架里的方法,以这些前缀开头的方法,会创建并拥有对象,这种命名约定也适合自己定义的方法.
- alloc
- new
- copy
- mutableCopy
//用alloc方法创建并拥有对象
id obj = [[NSObject alloc] init];
//现在你已经拥有这个对象了
通过用alloc类方法,你创建一个对象并拥有了它.变量obj有一个指针指向了这个创建的对象.你也可以用类方法new.
//创建对象并拥有它
id obj = [NSObject new];
//现在你已经拥有这个对象了
NSObject的实例方法”copy”创建一个对象的复制,被创建的对象所属的类必须遵循NSCopying协议,并且协议中的方法copyWithZone:必须被适当地实现.相似的NSObject的对象方法”mutableCopy”创建一个对象的可变复制,被创建的对象所属的类必须遵循NSMutableCopying协议,并且协议中的方法mutableCopyWithZone:必须被适当地实现.copy和mutableCopy的区别类似于NSArray和NSMutableArray的区别.这两个方法和alloc以及new创建对象的方式一样,所以你也拥有它们创建的对象.
上文提到的前缀方法的例子有
- allocMyObject
- newThatObject
- copyThis
- mutableCopyYourObject
然而,下面这些方法不符合这种命名约定
- allocate
- newer
- copying
- mutableCopyed
用retain来拥有一个对象
有些方法呢,不属于alloc/new/copy/mutableCopy开头的一类,但是它们最后也会return一个对象.这种情况你没有创建这个对象,所以你没有拥有它.例如NSMutableArray的实例方法array
//获取一个不是你创建或拥有的对象
id obj = [NSMutableArray array];
//这个获取的对象已经存在,但是你没有拥有它
变量obj有一个指向NSMutableArray对象的引用,但是你没有拥有它.为了拥有它,你必须用retain方法
//获取一个不是你创建或拥有的对象
id obj = [NSMutableArray array];
//这个获取的对象已经存在,但是你没有拥有它
[obj retain];
//现在,你已经拥有这个对象了.
*注意:
- 返回结果是对象的方法要分类讨论,到目前为止,主要有三类
- 1.以上面四个前缀开头的一类方法,这种方法返回对象并赋给某个变量后,这个变量有指向这个对象的引用,并且已经拥有这个对象.
- 2.retain
- 3.当前缀非上面是四种前缀的方法返回一个对象并赋给某变量后,这个变量拥有指向这个对象的引用,但是并不拥有这个对象.
- 上面可以看到,即使有的方法会返回一个对象给某变量,这个变量指向了这个新创建的对象,但是只要这个返回对象的方法不符合前文提到的alloc/new/copy/mutableCopy开头的一类方法,那么就不会拥有这个返回的对象*
当不再需要时,必须放弃对拥有对象的所有权
当你拥有一个对象但是并不再需要它的时候,你必须用release方法来释放对它的所有权.
//创建并拥有一个对象
id obj = [[NSObject alloc] init];
//现在,你已经拥有这个对象
[obj release];
//这个对象被释放了
//尽管这个变量obj拥有指针指向这个对象,但是你不能再访问这个对象了.
上面是用release对用alloc创建并拥有的对象进行释放.对于用retain而拥有的对象,可以用同样方式释放所有权
释放对retained对象的所有权
//获取一个不是创建或拥有的对象的所有权
id obj = [NSMutableArray array];
//获得的对象已经存在,但是你不拥有它
[obj retain];
//现在,已经拥有这个对象了
[obj release];
//对象的所有权被释放
//你不能再访问这个对象(注意:这里应该是指,自动释放池已经释放之后,然后再release之后,因为如果当前自动释放池没有释放的话,数组对象的引用计数仍然为1,还没有被销毁掉,仍然可以访问)
用alloc/new/copy/mutableCopy创建并拥有的对象,以及用retain方法拥有的对象,都要用release释放.
下面看一看一个方法是怎么返回一个被创建的对象的.
释放被retained的对象
下面例子展示了方法是如何返回一个被创建的对象的.(意思是说在方法内部创建并返回)
-(id)allocObject
{
//创建并拥有这个对象
id obj = [[NSObject alloc] init];
//此时,*这个方法*拥有这个对象
return obj;
}
如果一个方法返回一个对象,并且这个方法拥有这个对象,,那么这种拥有权就会传递给调用者.注意:为了是alloc/new/copy/mutableCopy一类的方法,本方法是以alloc开头的.
//拥有一个不是你自己创建并拥有的对象
id obj1 = [obj0 allocObject];
//现在,你拥有了这个对象
调用了allocObject意味着你创建了一个对象,并拥有了它,因为这个方法是以alloc开头的.
下面来看看类似[NSArray array]的array方法是怎么实现的.
返回一个没有拥有权的对象
[NSMutableArray array]方法返回一个新的对象,调用者并没有对这个新对象的拥有权.让我们看看我们如何实现这种方法.
我们不能用alloc/new/copy/mutableCopy为开头的前缀来声明这种方法.下面的例子中,用”object”作为方法名:
-(id)object
{
id obj = [[NSObject alloc]init];
//此时,这个方法拥有这个对象
[obj autorelease];
//这个对象仍然存在,但是你不拥有这个对象
return obj;
}
为了实现这种方法,我们用了autorelease方法.通过调用autorelease方法,你可以返回没有拥有权的被创建的对象.Autorelease提供了一种机制,这种机制使得当对象生命终结的时候,会被释放.
注意:图中当调用autorelease的时候,会在autorelease pool中注册这个对象,当autorelease pool被销毁的时候,被注册的对象再被销毁.
例如.NSMutableArray的类方法array的实现,应该就是如此.请注意,为了符合命名规则,这个方法并没有用alloc/new/copy/mutableCopy开头
id obj1 = [obj0 object];
//获取的对象已经存在,但是你并没有拥有它.
你可以通过retain来拥有被autorelease的对象.
id obj1 = [obj0 object];
//获取的对象已经存在,但是你并没有拥有它.
[obj1 retain];
//现在,你拥有了这个对象.
以后的章节,会更详细的讲解autorelease细节.
一定不要释放你不拥有的对象
就像前面描述的,当你拥有对象时,必须通过release方法来释放他们.但是,当你通过其他方式获得这个对象时,你一定不要调用release来释放他们.如果你这么做,应用会crash.例如释放一个你拥有的对象后,当你再次释放它时,应用会crash
//你创建并拥有一个对象
id obj = [[NSObject alloc] init];
//现在,你已经拥有这个对象了
[obj release];
//对象被释放掉了
[obj release];
/*
*你释放了这个对象,然而这个对象已经不再是你拥有的
*这个应用将会crash
*应用将会在这种情况下crash掉:
*当你对一个已经销毁的对象调用release方法
*当你访问一个已经销毁的对象
*/
当然,下面是一个释放你不拥有对象的例子:
id obj1 = [obj0 object];
//获取的对象存在,但是你不拥有它
[obj1 release];
//你释放了你不拥有的对象
//应用会crash掉
正如上述例子所示,你一定不要释放你不拥有的对象.这会引起应用crash掉.
我们已经学习了用引用计数进行内存管理时要考虑的规则,下面我们学习alloc,retain,release,以及dealloc是如何实现以及如何工作的.
实现alloc,retain,release,以及dealloc
OSX和iOS的很多组成部分在Apple Open Source是可以获得的.正如上面提到的,alloc,retain,release,以及dealloc是NSObject类的方法.NSObject是在Cocoa框架里的Foundation框架中的.不幸的是,Foundation框架并没有在Apple Open Source开源.幸运的是Core Foundation框架是Apple Open Source的一部分,NSObject中用于内存管理的代码是开源的.然而,没有NSObject本身的实现过程,我们很难理解全貌.所以让我们来看看GNUstep中的替代源代码.
GNUstep是一个和Cocoa框架完全兼容的实现.尽管我们不能期望它完全和苹果的实现一致,但是它用相同的方法,并且实现应该很相似.GNUstep源代码帮我们猜测苹果Cocoa的实现.
alloc方法
让我们以GNUstep中的NSObject的alloc方法开始.注意,本书中某些源代码有所更改是的重要部分更加清晰.
“alloc”方法这样调用:
id obj = [NSObject alloc];
在NSObject.m中alloc的实现是这样的:
GNUstep/modules/core/base/Source/NSObject.m alloc
+(id)alloc
{
return [self allocWithZone:NSDefaultMallocZone()];
}
+(id)allocWithZone:(NSZone*)z
{
return NSAllocateObject(self,0,z);
}
Apple, “Apple open source,” http://opensource.apple.com/
GNUstep, “GNUstep.org,” http://gnustep.org/
在allocWithZone:方法中,对象是用NSAllocateObject函数来分配的.实现如下
GNUstep/Modules/Core/Base/Source/NSObject.m NSAllocateObject
struct obj_layout {
NSUInterger 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];
//解释下最后一句,将new的第1个字节的地址返回,并且这个地址会强转为id类型,而原来的new会强转为struct obj_layout* 类型
}
/*
*void *memset(void *s, int ch, size_t n);
*函数解释:将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASCII值, 第一个值为指定的内存地址,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作, 其返回值为指向s的指针。
*作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
*/
NSAllocateObject函数调用NSZoneMalloc来分配一个内存空间.然后,这个控件会被填充0,然后空间指针被返回.
注意:最开始的时候,NSZone是用来防止内存碎片化的(如下图),通过根据实际情况在zones之间进项切换(分配内存),内存的分配会更有效率.
但是今天,当你看”Transition to ARC Release Notes”的时候,OC的runtime已经忽视了zones了.因为现在runtime内存管理算法已经足够高效,用zones已经没有意义.
移除掉NSZone相关的代码后,alloc方法简化如下
struct obj_layout{
NSUInteger retained;
{
+(id)alloc
{
int size = sizeof(struct obj_layout)+size_of_object;
struct obj_layout *p = (struct obj_layout *)calloc(1,size);
return (id)(p+1);
}
/*
*函数原型:void *calloc(size_t n, size_t size);
*功 能:在内存的动态存储区中分配n个长度为size的连续空间,函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL。
*分配的size是以struct_layout起始的,向后移动一位的指针返回,id类型
*/
retain方法
alloc方法返回用0填充的内存块,这个内存块的头是一个obj_layout结构体,结构体里面有个变量”retained”,来存储引用数的.这个数字就被称作引用计数.下图展示了GNUstep的对象的是实现的结构
你可以通过retainCount方法来得到引用计数的值
id obj = [[NSObject alloc] init];
NSLog(@"retainCount = %d",[obj retainCount]);
//打印:retainCount = 1
alloc刚刚被调用的时候,引用计数值是1.下面的源代码展示了GNUstep中retainCount函数是怎么实现的
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;
}
这段代码先通过对象的指针找到对象头,然后得到这个retained值,见下图
因为内存块刚被初始化的时候是被0填充的,所以retained的值是0.retainCount函数通过”NSExtraRefCount(self)+1”返回1.我们可以猜测,”retain”或”release”方法都是通过+1和-1来更改这个值的.
[obj retain];
下面来看看retain的实现
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 == UINTMAX - 1)
[NSException raise:NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"];
((struct obj_layout *)anObject)[-1].retained ++;
}
“Although it has a few lines of code to throw an exception, when the variable “retained” overflows, it is fundamentally incremented by one on the line “retained++”. ”
尽管当变量”retained”溢出的时候,有几行代码用来抛出异常,但是一般会执行”retained++”这句代码来在retained基础上+1.
下面学习下”release”方法
release方法
我们可以很轻易就能猜出”release”方法会有”retained–”.当然,当引用计数值变为0的时候,应该还有其他代码
[obj release];
下面是release的实现
-(void)release
{
if(NSDecrementExtraRefCountWasZero(self));
[self dealloc];//当引用计数为0时,向对象发送dealloc方法,进行销毁
}
BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
if(((struct obj_layout *)anObject)[-1].retained==0){
return YES;
}else{
((struct obj_layout *)anObject)[-1].retained--;
return NO;
}
}
//注意:当引用计数为0之后
//1.再次release已经销毁的对象
//2.访问已被销毁的对象
正如我们所期望的,”retained”每次会被-1;如果值变成0,这个对象会被”dealloc”方法销毁.下面看看dealloc方法是怎么实现的
dealloc方法
下面是”dealloc”方法的实现
- (void)dealloc
{
NSDeallocateObject(self);
}
inline void
NSDeallocateObject(id anObject)
{
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];//取出整个内存块
free(0);//释放掉
}
它只是将一个内存块释放掉.
我们已经看到了在GNUstep中alloc,retain,release,以及dealloc的是实现,下面是总结
- 所有的OC对象有一个值叫引用计数
- 当alloc/new/copy/mutableCopy或retain中的某个被调用的时候,引用计数会+1
- 当release被调用的时候,引用计数-1
- 当引用计数变为0的时候dealloc会被调用.
下面我们看看苹果的实现
苹果的alloc,retain,release,以及dealloc的实现
正如前面讲到的,NSObject类本身的源代码没有开源.我们用iOS查口德调试器(xcode debugger(lldb))来探索一下.首先在NSObject的alloc方法处设置一个断点.看看调试器中发生了什么.下面是在alloc内部调用的函数列表
+alloc;
+allocWithZone:
Class_createInstance
Calloc
NSObject类方法alloc调用allocWithZone.然后,通过class_createInstance函数(这个函数在runtime文档中有解释),调用calloc函数来分配内存块.看起来和GNUstep的是实现没什么不同.在开源的objc4库的runtime/objc-runtime-new.mm中可以查看class_createInstance的代码
那么NSObject的实例方法retainCount,retain,以及release呢?下面的函数会在内部被调用:
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(当然,当CFBasicHashRemoveValue返回0 的时候-dealloc将会被调用)
在所有上面的方法中,__CFDoExternRefOperation函数都被调用.然后这个函数调用命名相似的函数.这些函数是公共的.正如你所见,如果函数名字以CF开头,你将会在Core Foundation Framework中看到其源代码.下面的代码是CFRuntime.c中简化__CFDoExternRefOperation的实现
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函数.苹果的实现通过一个hash表(引用计数表)来处理引用计数的.如下图
注意:途中描述的,是以内存块的地址为key,通过这个key来寻找其引用计数,上面再实现调度函数的时候,第一句就是获取hash表”CFBasicHashRef table = get hashtable from obj;”,然后下满每个被调度的函数中,都传入这个hash表,和对象,通过对象就很容易在hash表中找到其引用计数.
在GNUstep的实现中,引用计数在每个对象内存块的头位置.但是在苹果的实现中,所有的引用计数都被存储在hash表(猜测:类似字典)的一个条目中.尽管GNUstep的实现看起来更简单更快捷,但是苹果的实现也有其自身的有点.
下面是GNUstep的实现中,将引用计数存储在对象的头位置时的有点:
- 更少的代码量
- 管理对象的生命周期更加简单,因为每个引用计数的内存区域都在对象的内存区域中.
但是像苹果似的存储在hash表中的优点是什么呢? - 每个对象没有头,因此不需要担心头区域的对齐问题
- 通过对hash表的条目的迭代,每个对象的内存块会被找到
第二条在调试的时候尤其有用.当有些对象的内存区域被销毁,但是hash表仍然存在的时,调试器就可以找到对象的指针.
当然,为了检测内存泄露,instruments可以检查hash表的条目,然后确定每个对象是否被”人”拥有.(当不被”人”拥有,但仍然存在于内存中时,这个对象就产生了内存泄露了.)
这就是苹果的实现.既然我们对苹果的实现有了更好的理解,那么还有一个内存管理的要点需要学习:autorelease;
autorelease
因为这个名字,你也许会认为autorelease是类似于ARC的一个东西.但是那是错误的想法.其实它更像C中的”自动变量”.
先回顾一下C中的自动变量.我们然后在看看GNUstep的代码来理解autorelease是真么工作的,然后再学习苹果的实现.
自动变量
一个自动变量在词法上是一个局部范围的变量(lexically scoped variable),当执行离开了这个作用域之后,这个变量会自动的销毁.
{
int a;
}
//因为已经离开了变量的作用域
//自动变量"int a"已经被销毁,然后也不能再访问它
用autorelease,你可以像自动变量一样以相同的方式来使用对象.也就是说,当执行离开一个代码块的时候,”release”方法会自动的调用给对象.你也可以控制这个代码块.
下图给出了怎么用autorelease方法的步骤:
- 创建一个NSAutoreleasePool对象
- 调用”autorelease”给某个已经分配好内存的对象
- 销毁NSAutoreleasePool对象
在创建NSAutoreleasePool对象和销毁NSAutoreleasePool对象之间的代码块相当于C中的变量范围.当NSAutoreleasePool对象被销毁的时候,release方法会自动的被调用给所有被autorelease的对象.下面是一个例子:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
在最后一行中,[pool drain]中会有[obj release].
在Cocoa框架中,处处有NSAutoreleasePool对象被创建,拥有,销毁的过程(就是说框架中已经有了,不需要手动创建,拥有,销毁..),例如NSRunLoop,这是整个引用的主循环,(因为每次运行循环都会创建,拥有,销毁一个NSAutoreleasePool),所以你不需要显性地自己再手动使用NSAutoreleasePool.
但是当有太多的autorelease对象的时候,应用的内存使用会变得非常紧张.因为在NSAutoreleasePool对象销毁之前这些对象一直存在.一个一般的例子是在(从文件系统)加载(到内存)和调整图片时.很多的autorelease对象,例如读取文件后的NSData对象,图像数据转换后的UIImage对象,以及调整图片后的对象都同时存在于内存中
for (int i=0;i<numberOfImage;++i){
/*
*处理图片,例如从文件中读取
*同时存在大量的autorelease对象
*因为NASutoreleasePool对象还没有被销毁
*在某个时间点,就会引起内存短缺
*/
}
这种情况下,你应当在适当的时间自己创建并销毁NSAutoreleasePool对象
for(int i=0;i<numberOfImages;i++){
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/*
*从文件系统(或网络)加载图片数据
*大量的autorelease对象同时存在
*/
[pool drain];
/*
*所有的autorelease对象都被[pool drain]释放掉了
}
在Cocoa框架中,你将会看奥很多类方法返回autorelease对象,例如NSMutableArray的类方法”arrayWithCapacity”.
id array = [NSMutableArray arrayWithCapacity:1];
等价于下面的代码
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
//注意:initWithCapacity:方法中,所有权可以传递给array,但是经过autorelease后,所有权就不能传递给array
//在此,可以总结一下,所谓的"拥有""所有权"指的是用完对象之后"必须释放(release)的义务"
//比如上面两段代码,array虽然指向了这个对象,但是不需要它释放,由自动释放池释放,所有a array没有"释放的义务",即没有"拥有"这个对象
实现autorelease
本节,我们讨论下GNUstep中autorelease的实现,正如前面我们队alloc,retain,release以及dealloc所做的那样,来学习下它是怎么工作的.
[obj autorelease];
这行代码调用了NSObject的实例方法”autorelease”.下面源代码展示了autorelease方法是怎么是实现的
- (id)autorelease
{
[NSAutoreleasePool addObject:self];
}
事实上,autorelease仅仅是调用了NSAutoreleasePool类方法addObject.实际上,在GNUstep中,为了优化的目的,它的实现有点不一样.看下面解释.
OC方法调用的优化
在GNUstep中,autorelease方法的实现用一种不同寻常的方式,已达到优化的目的.因为autorelease在iOS和OSX中很频繁的被调用,所以有一个焦作IMP缓存的特殊机制.当框架被初始化的时候,它缓存下很多搜索结果,例如函数指针,以及类和方法的名称解析.如果这种机制不存在,那么当autorelease被调用的时候,这个过程(指解析名称以及搜索函数指针等搜索操作)都得执行一次.
id autorelease_class = [NSAutoreleasePool class];
SEL autorelease_sel = @selector(addObject:);
IMP autorelease_imp = [autorelease_class methodForSelector:autorelease_sel];//methodForSelector:"Locates and returns the address of the receiver’s implementation of a method so it can be called as a function"
当方法别调用的时候,它仅仅返回一个缓存值
- (id)autorelease
{
(*autorelease_imp)(autorelease_class,autorelease_sel,self);
}
上面就是方法调用IMP缓存.如果IMP缓存没有的话,上面代码可以被重写成下面形式.根据环境不同,比起IMP缓存,重写的这种形式大约需要花费两倍的时间
- (id)autorelease{
[NSAutoreleasePool addObject:self];
}
让我们看一下NSAutoreleasePool类的实现,下面是NSAutoreleasePool的简化实现
+ (void)addObject:(id)anObj{
NSAutoreleasePool *pool = geting active NSAutoreleasePool;//先获取活动的NSAutoreleasePool对象
if(pool != nil){
[pool addObject:anObj];//如果存在,调用实例方法addObject:将对象加入到这个活动NSAutoreleasePool对象中
}else{
NSLog(@"autorelease is called without active NSAutoreleasePool.");
}
}
类方法”addObject:”调用了目前处于活动状态的NSAutoreleasePool对象的NSAutoreleasePool类的实例方法”addObject:”.在下面的例子中,变量”pool”是活动的NSAutoreleasePool对象.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
当多个NSAutoreleasePool对象被床架并嵌套时,最里面的对象就成为当前活跃的pool.下面的例子中pool2是活跃的pool
NSAutoreleasePool *pool = [[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];
下面我们看看NSAutoreleasePool的实例方法addObject:是怎么实现的.
GNUstep/Modules/Core/Base/Source/NSAutoreleasePool.m addObject
- (void)addObject:(id)anObj{
[array addObject:anObj];
}
它将这个对象添加到了一个可变数组中.在最初的GNUstep的实现中,用的不是数组,而是链表.不管怎样,被autorelease的对象呗放到了一个容器中,也即是说,当NSObject的实例方法”autorelease”被调用的时候,这个对象就会被加入到一个活动的pool的容器中.
[pool drain];
下面,我们看看活动的NSAutoreleasePool对象当被调用drain时,是怎么销毁的
- (void)drain
{
[self dealloc];
}
- (void)dealloc{
{
[self emptyPool];
[array release];
}
- (void)emptyPool
{
for(id obj in array){
[obj release];
}
}
//疑问:NSMutableArray被release的时候不是先将数组中元素先release掉吗?那么这里的emptyPool是否是多余?
我们可以看到,”release”方法被调用给每个在pool中的对象.
autorelease的苹果实现
我们可以在objc4库的runtime/objc-arr.mm中看到苹果的autorelease实现.下面是实现代码
objc4/runtime/objc-arr.mm class AutoreleasePoolPage
class AutoreleasePoolPage
{
static inline void *push()
{
/*与NSAutoreleasePool对象的创建及拥有相关*/
}
static inline void pop (void *token)
{
/*与NSAutoreleasePool对象的销毁相关*/
releaseAll();
}
static inline id autorelease(id obj){
/*与NSAutoreleasePool的类方法addObject:相关的*/
AutoreleasePoolPage *autoreleasePoolPage = /*获得当前活跃的AutoreleasePoolPage对象*/
autoreleasePoolPage->add(obj);
}
id *add(id obj)
{
/*添加obj对象到内部数组*/
}
void releaseAll()
{
/*向内部数组中的所有对象调用release*/
}
};
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
id objc_autorelease(id obj)
{
return AutoreleasePoolPage::autorelease(obj);
}
这些函数以及AutoreleasePoolPage类是用C++类以及一个动态数组实现的.这些函数和GNUstep做类似的工作.正如我们之前那样,用调试器,我们可以探究到在autorelease和NSAutoreleasePool的类方法中调用了什么函数.这些方法将会调用与autorelease相关的objc4函数:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/*等价于objc_autoreleasePoolPush()*/
id obj = [[NSObject alloc] init];
[obj autorelease];
/*等价于objc_autorelease(obj)*/
[pool drain];
/*等价于objc_autoreleasePoolPop(pool)*/
与此同时,在iOS中,NSAutoreleasePool类有一个方法可以检查autoreleased对象的状态.这个方法叫showPools,会将NSAutoreleasePool的状态展示在控制台.只能用作调试目的,因为这是个私有方法.可以这么用:
[NSAutoreleasePool showPools];
在最新的OC runtime中,_objc_autoreleasePoolPrint()代替”showPools”,因为showPools只能在iOS中工作.这个方法也是个私有方法,所以还是只能用作调试目的.
/*声明函数*/
extern void _objc_autoreleasePoolPrint();
/*真是autoreleasePool的状态,调试目的*/
_objc_autoreleasePoolPrint();
然互你就会看到AutoreleasePoolPage的状态.下面是结果.
知道某个对象是autoreleased与否很重要,就像下面说的
autorelease NSAutoreleasePool 对象
问题:如果”autorelease”被调用给NSAutoreleasePool对象会怎样?
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool autorelease];
答案:应用将会停止
*结束app因为无法捕捉的异常:’NSInvalidArgumentException’
原因:”* -[NSAutoreleasePool autorelease]:不能autorelease一个autorelease pool”
当用OC在一个Foundation框架中的对象上调用autorelease时,几乎都会调用NSObject实例方法,也就是调用NSObject类中定义的autorelease方法.然而NSAutoreleasePool类重写了autorelease方法,当autorelease被调用在autoreleasepool上时,显示错误.
总结:
本章学习了下面知识点
- 用引用计数进行内存管理的概念
- alloc,retain,release,以及dealloc方法的实现
- autorelease的机制以及实现
即使是ARC引入之后,这些知识点仍然很重要.下一章,我们将会学习ARC的引入带来了什么变化.
以上是关于ARC以前的故事的主要内容,如果未能解决你的问题,请参考以下文章