第一章 ARC以前的故事

苹果是在OSX狮子和iOS5之后才提供的ARC,全称叫自动内存管理.

ARC:

- 让编译器来替代程序猿进行内存管理.

- 这极大的提高了效率.

-

本章先看看什么是MRC—ARC出来之前的内存管理世界—手动内存管理.


引用计数

-解释下,这个”引用”,大体意思应该类似于指针”指向”的意思.

以教室内的灯举例子:

1. 当第一个人进入教室的时候,计数+1,引用计数从0变为1,开灯

2. 当另一人进入教室的时候,+1,引用计数从1变为2.

3. 当某人从教室离开时,计数-1,引用计数从2变为1.

4. 当最后一人离开时,计数变为0,关灯.

运用到内存管理上,上文中的每个人可以看做是一个OC上下文.

上下文:这个词用来表示一段程序代码,一个变量,一个变量范围,或一个对象.表示对一个目标对象的处理.

进一步探索内存管理

不需要记忆引用计数的数目,牢记下面规则:

  1. 你拥有你创建的对象的所有权 注:以后直接用”拥有”表示”拥有…的所有权”
  2. 用retain来拥有一个对象的所有钱
  3. 当不再需要时,必须放弃你拥有的对象的所有权
  4. 无权放弃你不拥有的对象的所有权
对象操作 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.当前缀非上面是四种前缀的方法返回一个对象并赋给某变量后,这个变量拥有指向这个对象的引用,但是并不拥有这个对象.
  • 上面可以看到,即使有的方法会返回一个对象给某变量,这个变量指向了这个新创建的对象,但是只要这个返回对象的方法不符合前文提到的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];
//对象的所有权被释放
//你不能再访问这个对象

用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];
}
/*
*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++”. ”

尽管只有很少的一点diamante来抛出异常,但是当变量”retained”溢出的时候…??(怎么翻译?)

下面学习下”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方法的步骤:

  1. 创建一个NSAutoreleasePool对象
  2. 调用”autorelease”给某个已经分配好内存的对象
  3. 销毁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的引入带来了什么变化.


时间: 2024-11-13 08:15:32

第一章 ARC以前的故事的相关文章

《大道至简》第一章读后感

经常听见有人抱怨编程太难,说自己不是学软件的料,那么他们真该好好看看<大道至简>这本书,相信他们看完这本书后会有很大收获. <大道至简>第一章引用了一个很简单的故事“愚公移山”,用这个故事很好的概述了我们在完成一个项目时所要进行的步骤.听上去“愚公移山”和编程简直是风马牛不相及,但是看过作者的叙述又有原来如此的感觉.其实编程并没有什么难懂的,就和我们日常生活一样,发现问题,分析问题,提出解决问题的方案,实施,和后续的验收.例如某天我们突然发现家里放不出水了,这就是发现问题,我们会观

读《大道至简》第一章有感

近期老师为我们推荐了一本书,叫做<大道至简>,书很薄,却精辟的讲述了软件工程专业对于编程这一实践过程的重要思想,我们总以为编程是怎样的有难度,却从来没有考虑过编程为什么难,他到底难在哪.事实是我们错了,我们不是不会技术,而是不会方法. 第一章主要讲述了编程的精义,作者利用愚公移山的故事,简洁明了的讲解了编程的实质含义,从愚公移山实施的各个方面与编程联系在一起,讲解了编程的过程.从开始的原因,到编程实现的目标,然后小组团队对这个编程项目的讨论,以及各个人员在不同项目上的安排,并且还有项目之外的协

大道至简第一章读后感 Java伪代码形式

观看了大道至简的第一章之后,从愚公移山的故事中我们可以抽象出一个项目, 下面用Java 伪代码的形式来进行编写: import java(愚公移山的故事) //愚公移山 public class yugong { //项目的目的:惩山北之塞,出入之迂: //项目的基本沟通方式:聚室而谋曰: //愚公确定的项目的目标:毕力平险,指通豫南,达于汉阴: //项目的技术方案:扣石垦壤,箕畚运于渤海之尾: //项目中的三名技术人员以及工程管理人员:(愚公)率子孙荷担者三夫: //力量较弱,富有激情的外援:

花无涯带你走进黑客之 小白入门 第一章

最近开始有一个想法, 想谈谈小白如何慢慢学习网络安全相关知识, 有正确得价值观,做正确的事情. 初心也是为了帮助更多人学习到黑客攻防,学会保护自己和身边的人. 写一些自己的分享和经验,每一期可能都有时间就进行更新,感谢大家的支持! 相信每一个对计算机感兴趣的童鞋都有着一颗黑客的心, 我也不例外, 我希望通过一系列的文章让大家了解黑客和网络安全. 不是很会写一些感人故事心得, 更想是通过自己得分享也顺便提升自己 -.- 不是很喜欢在文章里头加特别花哨 过多的图片,也不会怎么配图... 可能阅读起来

《大道至简》第一章编程的精义伪代码读后感

最近,读了老师推荐的一本关于编程思想的书<大道至简>.书中第一章主要讲编程的精义,以愚公移山的故事来形象的讲解编程的的过程.通过一个简单的寓言故事,看到原始需求的产生,项目的沟通,项目目标,制定解决方案,外力协助,这些也都是编程项目的过程.从中我们看到了编程的根本:顺序,分支和循环. import.java.大道至简.*; import.java.愚公移山.*; public class Yugongyishan { 愚公={项目组织者,团队经理,编程人员,技术分析师}; //沟通方式:聚室而

大道至简第一章观后感

大道至简读后感 大道至简的作者用了很短的篇幅把其在软件开发方面的思考和感悟写了出来,他直指本源的讲述了编程技术.更为可贵的是作者不使这本讲高技术的书变得枯燥无味:而是让读者读起来幽默风趣. 第一章作者讲述了编程的精义,作者在第一章分5步讲述了编程的精义.首先作者用愚公移山的故事阐述了如何去编程的思路与步骤,编程首先是用来满足人们的原始需求(惩山北之塞,出入之迂.):在编程中需要团体的沟通(聚室而某):而后需要确定编程项目的目标(毕力平险,指通豫南,达于汉阴)并且讨论技术方案确定人员.同智叟的交谈

读大道至第一章简有感

大道至简是一本由生活实例和比较通俗易通的文字来阐述编程的书,其中的编程精义便是:仅仅就编程而言,实在是一种很简单的事,甚至是一件劳力活. 它通过寓言故事<愚公移山>来告诉我们编程要有耐心,要有恒心,不怕吃苦.我们从中看到了编程的精义:顺序,分支 和循环.庞大若“愚公移山”这样的工程,都是可以通过这样简单的编程来实现的,这,就是编程的精义了. 关于会不会写程序的问题,第一章给出了很简单的回答,其实编程就隐藏在我们的生活中,我们生活中的每一步都有编程的顺序.所以除了先天智障或后天懒惰者,都是可以学

《大道至简》第一章读后感,java伪代码形式

import java.大道至简.*; import  java.io.*; import.java.愚公移山.*; public class YuGongYiShan { public static void main(String [] args) throws IOException { int  愚公: int  子孙: int 山=1: while(山==1&&山不增加) { if(愚公死) {有子存焉,子孙去完成移山的任务,何苦而不平,这个工程必定会实现:} else {愚公自

大道至简第一章读后感JAVA伪代码形式

观看了<大道至简>第一章后,从愚公移山的故事中可以发现愚公移山其实可以看作是一个一个项目,下面用JAVA伪代码形式编写: Import java(愚公移山) //愚公移山 public class yugongyishan { //需求:惩山北之塞,出入之迂; //项目沟通的基本方式:聚室而谋曰; //项目最终目标:毕力平险,指通豫南,达于汉阴; //择定的技术方案:叩石垦壤, 箕畚运于渤海之尾; //项目中的技术人员和管理人员:(愚公)率子孙荷担者三夫; //满富工作激情的外协: 邻人京城氏