引用计数与内存管理

转载自(http://blog.sina.com.cn/s/blog_6de189920100zpxr.html)



内存管理不是一个容易解决的问题,虽然cocoa的解决方案非常简洁,但是想精通还要费些时日.在Mac OS X v10.5以及之后的版本苹果新增加了垃圾回收机制用来对内存进行管理.ios开发不支持垃圾回收机制.

在内存管理这块,cocoa引入了一种称为引用计数(reference counting)的技术,有时也叫保留计数,每个对象有一个与之对关联的整数,称作它的引用计数器或保留计数器.当某段代码要访问一个对象的时候,该代码将该对象的保留计数值加1,表示"我要访问该对象",当这段代码结束对象访问时,将对象的保留计数值减1,表示它不再访问该对象,当保留计数值为0时,表示不再有代码访问对象了,对象被销毁,其占用的内存被系统回收.


当使用alloc,new方法或者通过copy消息创建一个对象时,对象的保留计数值被设置为1,要增加对象的保留计数值,可以给对象发送一条retain,减少保留计数值时,发送一条release消息或者autorelease消息.当一个对象保留计数值为0(即将被销毁)时,objective-c会自动向发送一条dealloc消息.要获取保留计数器的数值可以使用retainCount消息来获取.

- (id)retain;

- (void)release; 

- (id)autorelease;

- (unsigned)retainCount;


对象可以接受任意多的retain和release,只要计数器的值是正的.

MyClass *object = [[MyClass alloc] init];//count =1

[object retain];//count =2

[object retain];//count =3

[object release];//count =2

[object release];//count =1

[object retain];//count =2

[object release];//count =1

[object release];//count =0,dealloc it


这样看起来可能内存管理很简单.但是当你考虑对象所有权的时候,就会变的复杂了.

我们先来看下cocoa内存管理规则

(1)当你使用new,alloc,copy方法创建一个对象时,该对象的保留计数值为1,当不再使用该对象时,你要负责向该对象发送一条release或者autorelease消息.

(2)当你通过任何方法获得一个对象时,假设该对象保留计数值为1,而且已经被设置为自动释放,你不需要执行任何操作来确保对象被清理.,如果打算要在一段时间被拥有该对象,你需要保留它,并在操作完之后释放它.

(3)如果你保留了某个对象,你需要释放或者自动释放该对象,必须保持retain与release方法的使用次数相等.


autorelease

前面我们强调了,所有使用new, alloc,copy或者是 retain 增加计数器的对象都要用 [auto]release 释放。记得在 C++ 中,返回值可以在栈上,以便在离开作用域的时候可以自动销毁。但在 Objective-C 中不存在这种对象。函数使用 alloc 分配的对象,直到将其返回栈之前不能释放。下面的代码将解释这种情况:

// 第一个例子

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    Point2D* result = [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

    return result;

}

// 错误!这个函数使用了 alloc,所以它将对象的引用计数器加 1。

// 根据前面的说法,它应该被销毁,但是这将引起内存泄露:

[calculator add:[calculator add:p1 and:p2] and:p3];

// 调用[calculator add:p1 and:p2],没有办法 release。所以引起内存泄露。

 

// 第二个例子

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    return [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

}

// 错误!这段代码实际上和上面的一样,

// 不同之处在于仅仅减少了一个中间变量。

 

// 第三个例子

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    Point2D* result = [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

    [result release];

    return result;

}

// 错误!显然,这里仅仅是在对象创建出来之后立即销毁了。

这个问题看起来很棘手。如果没有 autorelease 的确如此。给一个对象发送 autorelease 消息意味着告诉它,在“一段时间之后”销毁。但是这里的“一段时间之后”并不意味着“任何时间”。现在,我们有了上面这个问题的一种解决方案:

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2

{

    Point2D* result = [[Point2D alloc] initWithX:([p1 pointX] + [p2 pointX])

                                            andY:([p1 pointY] + [p2 pointY])];

    [result autorelease];

    return result; // 更简短的代码是:return [result autorelease];

}

// 正确!result 将在以后自动释放


autorelease 池

刚才我们了解到 autorelease 的神奇之处:它能够在合适的时候自动释放分配的内存。我们先来了解 autorelease 的机制。当对象收到 autorelease 消息的时候,它会被注册到一个“autorelease 池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?答案是丰富多彩的:如果你使用 Cocoa 开发 GUI 界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。

拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用 Cocoa 创建 GUI 程序时,这个 autorelease 池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。如果没有 GUI,你必须自己建立 autorelease 池。当对象收到 autorelease消息时,它能够找到最近的 autorelease 池。当池可以被清空时,你可以对这个池使用 release 消息。一般的,命令行界面的 Cocoa 程序都会有如下的代码:

int main(int argc, char* argv[])

{

    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    //...

    [pool release];

    return 0;

}

注意在 Mac OS X 10.5 的 NSAutoreleasePool 类新增加了一个 drain 方法。在引用计数的环境中与release等价的.现在有两种环境:引用计数和垃圾回收。Mac OS 的新版本都会支持垃圾收集器,但是 iOS 却不支持。在引用计数环境下,NSAutoreleasePool 的 release 方法会给池中的所有对象发送 release 消息,如果对象注册了多次,就会多次给它发 release。drain 和 release 在应用计数环境下是等价的。在垃圾收集的环境下,release 不做任何事情,drain 则会触发垃圾收集。

我们在程序可以使用多个 autorelease 池,对象收到 autorelease 消息时会注册到最近的池。因此,如果一个函数需要创建并使用很大数量临时对象,为了提高性能,可以创建一个局部的 autorelease 池。这种情况下,这些临时变量就可以及时的被销毁,从而在函数返回时就将内存释放出来。


autorelease 的注意点

(1)非必要地发送多个 autorelease 类似发送多个 release 消息,在内存池清空时会引起内存错误,

(2)即使 release 可以由 autorelease 替代,也不能滥用 autorelease。因为 autorelease 要比正常的 release 消耗资源更多。另外,不必要的推迟 release 操作无疑会导致占用大量内存,容易引起内存泄露。


autorelease 和 retain

多亏了 autorelease,方法才能够创建能够自动释放的对象。但是,长时间持有对象是一种很常见的需求。在这种情形下,我们可以向对象发送 retain 消息,使用了 retain 之后,对象的生命期变长了(使用 retain 将使其引用计数器加 1),为了让对象能够正确地被释放,调用者必须负责后面手动的release将计数器再减 1。



Setters

如果不对 Objective-C 的内存管理机制有深刻的理解,在写setter的时候也会出现问题。假设一个类有一个名为 title 的 NSString 类型的属性,我们希望通过 setter 设置其值。这个例子虽然简单,但已经表现出 setter 所带来的主要问题:参数如何使用?


-(void) setString:(NSString*)newString

{

    self->string = newString; // 直接指定

}

外面传进来的对象仅仅使用引用,不带有 retain。如果外部对象改变了,当前类也会知 道。也就是说,如果外部对象被释放掉,而当前类在使用时没有检查是否为 nil,那么当前类就会持有一个非法引用。


使用 retain 指定

外部对象被引用,并且使用 retain 将其引用计数器加 1。外部对象的改变对于当前类也是可见的,不过,外部对象不能被释 放,因为当前类始终持有一个引用。

-(void) setString:(NSString*)newString

{

    self-string = [newString retain]// 使用 retain 指定

}

如下调用:NSString *object = [[NSString alloc] init];//count = 1

[otherObject setString:object];//count = 2

[object release];//count = 1

这时候就出问题了object的保留计数值还是1

使用copy指定,与上面还是一样的结果.


 

在这种情况下需要释放旧值才可以

-(void) setString:(NSString*)newString

{

    [self->string release];

    self->string = [newString retain];

    // 错误!如果 newString == string(这是可能的),

    // newString 引用是 1,那么在 [self->string release] 之后

    // 使用 newString 就是非法的,因为此时对象已经被释放

}


但是这时候还是不行,这时候再加一个判断如下:

-(void) setString:(NSString*)newString

{

    if (self->string != newString)

        [self->string release]// 正确:给 nil 发送 release 是安全的

  self->string = [newString retain] // 错误!应该在 if 里面

                                        // 因为如果 string == newString,

                                        // 计数器不会被增加

}

 

// ------ 正确的实现 ------

// 最佳实践:C++ 程序员一般都会“改变前检查”

-(void) setString:(NSString*)newString

{

    // 仅在必要时修改

    if (self->string != newString) {

        [self->string release]// 释放旧的

        self->string = [newString retain]// retain 新的

    }

}

 

// 最佳实践:自动释放旧值

-(void) setString:(NSString*)newString

{

    [self->string autorelease]; // 即使 string == newString 也没有关系,

                                // 因为 release 是被推迟的

    self->string = [newString retain];

    //... 因此这个 retain 要在 release 之前发生

}

 

// 最佳实践:先 retain 在 release

-(void) setString:(NSString*)newString

{

    [newString retain]// 引用计数器加 1(除了 nil)

    [self->string release]// release 时不会是 0

    self->string = newString; // 这里就不应该再加 retain 了

}


Getters

Objective-C 中,所有对象都是动态分配的,使用指针引用。一般的,getter 仅仅返回指针的值,而不应该复制对象。getter 的名字一般和数据成员的名字相同,这并不会引起任何问题。



垃圾收集器

Objective-C 2.0 实现了一个垃圾收集器。换句话说,你可以将所有内存管理交给垃圾收集器,再也不用关心什么 retain、release 之类。但是,不同于 Java,Objective-C 的垃圾收集器是可选的:你可以选择关闭它,从而自己管理对象的生命周期;或者你选择打开,从而减少很多可能有 bug 的代码。垃圾收集器是以一个程序为单位的,因此,打开或者关闭都会影响到整个应用程序。

如果开启垃圾收集器,retain、release 和 autorelease 都被重定义成什么都不做。因此,在没有垃圾收集器情况下编写的代码可以不做任何改变地移植到有垃圾收集器的环境下,理论上只要重新编译一遍就可以了。“理论上”意思是,很多情况下涉及到资源释放处理的时候还是需要特别谨慎地对待。因此,编写同时满足两种情况的代码是不大容易的,一般开发者都会选择重新编写。

由 Apple 开发的 Objective-C 垃圾收集器叫做 AutoZone。这是一个公开的开源库,我们可以看到起源代码。不过在 Mac OS X 10.6 中,垃圾收集器可能有了一些变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值