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上下文.

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

进一步探索内存管理

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

  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.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方法的步骤:

这里写图片描述

  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的引入带来了什么变化.

以上是关于ARC以前的故事的主要内容,如果未能解决你的问题,请参考以下文章

ARC简介以及工程中ARC与非ARC的混合

ARC简介以及工程中ARC与非ARC的混合

MRC和ARC混编

如何在像 snapchat 故事一样从底部滑动到顶部 android 时打开底部工作表片段

平铺 PDF 视图正在泄漏内存(非 ARC)

ARC自定义委托奇怪的问题