使用 Cocoa 和 Objective-C 理解引用计数

Posted

技术标签:

【中文标题】使用 Cocoa 和 Objective-C 理解引用计数【英文标题】:Understanding reference counting with Cocoa and Objective-C 【发布时间】:2008-08-09 03:13:04 【问题描述】:

我刚刚开始研究 Objective-C 和 Cocoa,以便使用 iPhone SDK。我对 C 的 mallocfree 概念相当满意,但 Cocoa 的引用计数方案让我相当困惑。有人告诉我,一旦你理解它就非常优雅,但我还没有完成。

releaseretainautorelease 是如何工作的,它们的使用约定是什么?

(或者失败了,你读了什么帮助你得到它?)

【问题讨论】:

【参考方案1】:

让我们从retainrelease开始; autorelease 只是了解基本概念后的特例。

在 Cocoa 中,每个对象都会跟踪它被引用的次数(具体来说,NSObject 基类实现了这一点)。通过在一个对象上调用retain,你是在告诉它你想将它的引用计数加一。通过调用release,您告诉对象您正在释放它,并且它的引用计数递减。如果在调用release 之后,引用计数现在为零,则系统会释放该对象的内存。

这与mallocfree 的基本不同之处在于,任何给定的对象都不需要担心系统的其他部分崩溃,因为您已经释放了它们正在使用的内存。假设每个人都在按照规则玩和保留/释放,当一段代码保留然后释放对象时,任何其他引用该对象的代码都不会受到影响。

有时可能令人困惑的是知道在什么情况下您应该致电retainrelease。我的一般经验法则是,如果我想在某个对象上挂起一段时间(例如,如果它是类中的成员变量),那么我需要确保对象的引用计数知道我。如上所述,对象的引用计数通过调用retain 递增。按照惯例,当使用“init”方法创建对象时,它也会增加(实际上设置为 1)。在这两种情况下,我有责任在完成后调用对象上的release。如果我不这样做,就会出现内存泄漏。

对象创建示例:

NSString* s = [[NSString alloc] init];  // Ref count is 1
[s retain];                             // Ref count is 2 - silly
                                        //   to do this after init
[s release];                            // Ref count is back to 1
[s release];                            // Ref count is 0, object is freed

现在为autorelease。自动释放被用作一种方便(有时是必要的)方法来告诉系统在一段时间后释放这个对象。从管道的角度来看,当调用autorelease 时,当前线程的NSAutoreleasePool 会收到调用警报。 NSAutoreleasePool 现在知道一旦它获得机会(在事件循环的当前迭代之后),它可以在对象上调用release。从我们程序员的角度来看,它会为我们调用release,所以我们不必(事实上,我们不应该)。

需要注意的重要一点是(同样,按照惯例)所有对象创建 class 方法都会返回一个自动释放的对象。例如,在下面的例子中,变量“s”的引用计数为 1,但在事件循环完成后,它将被销毁。

NSString* s = [NSString stringWithString:@"Hello World"];

如果你想挂在那个字符串上,你需要显式调用retain,然后在完成后显式调用release

考虑以下(非常人为的)代码,您会看到需要autorelease 的情况:

- (NSString*)createHelloWorldString

    NSString* s = [[NSString alloc] initWithString:@"Hello World"];

    // Now what?  We want to return s, but we've upped its reference count.
    // The caller shouldn't be responsible for releasing it, since we're the
    // ones that created it.  If we call release, however, the reference 
    // count will hit zero and bad memory will be returned to the caller.  
    // The answer is to call autorelease before returning the string.  By 
    // explicitly calling autorelease, we pass the responsibility for
    // releasing the string on to the thread's NSAutoreleasePool, which will
    // happen at some later time.  The consequence is that the returned string 
    // will still be valid for the caller of this function.
    return [s autorelease];

我意识到这一切有点令人困惑——不过,在某些时候,它会点击。这里有一些参考资料可以帮助您:

Apple's introduction 用于内存管理。 Cocoa Programming for Mac OS X (4th Edition),作者 Aaron Hillegas - 一本写得很好的书,里面有很多很好的例子。读起来像教程。 如果您真的想深入了解,可以前往Big Nerd Ranch。这是由上述书籍的作者 Aaron Hillegas 经营的培训机构。几年前我参加了可可介绍课程,这是一种很好的学习方式。

【讨论】:

您写道:“通过调用 autorelease,我们暂时增加了引用计数”。我认为这是错误的; autorelease 只标记将来要释放的对象,它不会增加引用计数:cocoadev.com/index.pl?AutoRelease "现在是自动释放。自动释放被用作一种方便(有时是必要的)方式来告诉系统在一段时间后释放这个对象。"作为引导句,这是错误的。它不会告诉系统“释放 [it] up”,而是告诉它减少保留计数。 非常感谢您的精彩解释。只有一件事还不清楚。如果NSString* s = [[NSString alloc] initWithString:@"Hello World"]; 返回一个自动释放的对象(如您所写),为什么我必须执行return [s autorelease]; 并再次将其设置为“自动释放”而不仅仅是return s @Stefan: [[NSString alloc] initWithString:@"Hello World"] 不会返回自动释放的对象。每当调用alloc 时,引用计数都会设置为1,并且该代码有责任确保它被释放。另一方面,[NSString stringWithString:] 调用确实返回一个自动释放的对象。 有趣的琐事:由于答案使用 @"" 和 NSString,因此字符串始终是恒定的,因此,绝对保留计数将是恒定的并且完全不相关......不会使无论如何,回答错误只会强化这样一个事实,即绝对保留计数永远不是您应该担心的事情。【参考方案2】:

如果您了解保留/释放的过程,那么有两条黄金法则对成熟的 Cocoa 程序员来说是显而易见的,但不幸的是,对于新手来说很少能清楚地阐明这一点。

    如果返回对象的函数的名称中有alloccreatecopy,那么该对象就是你的。完成后必须致电[object release]。或者CFRelease(object),如果它是一个核心基础对象。

    如果它的名称中没有这些词之一,则该对象属于其他人。如果您希望在函数结束后保留​​该对象,则必须调用 [object retain]

您最好在自己创建的函数中也遵循此约定。

(挑剔者:是的,不幸的是,有一些 API 调用是这些规则的例外,但它们很少见)。

【讨论】:

这是不完整且不准确的。我仍然无法理解为什么人们试图重复规则而不是简单地指向相关文档:developer.apple.com/documentation/Cocoa/Conceptual/MemoryMgmt/… Core Foundation 的规则与 Cocoa 的规则不同;见developer.apple.com/documentation/CoreFoundation/Conceptual/… 我也不同意。如果一个函数返回它不想拥有的东西,它应该自动释放它。保留它是函数作业的调用者(如果需要)。它应该与被调用的任何方法的名称无关。这更像是 C 风格的编码,对象的所有权不明确。 对不起!我认为我在否决投票时很仓促。 Memory Management Rules您的回答几乎引用了苹果文档。【参考方案3】:

如果您正在为桌面编写代码并且可以针对 Mac OS X 10.5,那么您至少应该考虑使用 Objective-C 垃圾回收。它确实会简化您的大部分开发工作——这就是为什么 Apple 首先投入所有精力来创建它,并使其表现良好。

关于不使用GC时的内存管理规则:

如果您使用+alloc/+allocWithZone:+new-copy-mutableCopy 创建一个新对象,或者如果您-retain 一个对象,您将获得它的所有权,并且必须确保它被发送到-release。 如果您以任何其他方式收到对象,则您不是它的所有者并且应该确保它是通过-release发送的。 如果您想确保发送对象-release,您可以自己发送,也可以发送对象-autorelease,当前自动释放池将发送-release (每收到一次-autorelease)当池耗尽时。

通常-autorelease 用于确保对象在当前事件的时间内存活,但之后会被清理,因为围绕 Cocoa 的事​​件处理有一个自动释放池。在 Cocoa 中,将对象返回给自动释放的调用者比返回调用者本身需要释放的对象更常见。

【讨论】:

【参考方案4】:

Objective-C 使用Reference Counting,这意味着每个Object 都有一个引用计数。创建对象时,它的引用计数为“1”。简单地说,当一个对象被引用(即存储在某个地方)时,它会被“保留”,这意味着它的引用计数会增加一。当一个对象不再需要时,它被“释放”,这意味着它的引用计数减一。

当一个对象的引用计数为 0 时,该对象被释放。这是基本的引用计数。

对于某些语言,引用会自动增加和减少,但objective-c 不是其中一种语言。因此程序员负责保留和释放。

一个典型的写方法是:

id myVar = [someObject someMessage];
.... do something ....;
[myVar release];
return someValue;

需要记住在代码中释放任何获取的资源的问题既乏味又容易出错。 Objective-C 引入了另一个旨在使这更容易的概念:自动释放池。自动释放池是安装在每个线程上的特殊对象。如果您查看 NSAutoreleasePool,它们是一个相当简单的类。

当一个对象收到发送给它的“自动释放”消息时,该对象会为当前线程寻找位于堆栈上的任何自动释放池。它会将对象作为对象添加到列表中,以在将来的某个时间向其发送“释放”消息,这通常是在池本身被释放时。

使用上面的代码,您可以通过以下方式将其重写为更短且更易于阅读:

id myVar = [[someObject someMessage] autorelease];
... do something ...;
return someValue;

因为对象是自动释放的,我们不再需要显式调用它的“释放”。这是因为我们知道一些自动释放池稍后会为我们完成。

希望这会有所帮助。***的文章关于引用计数非常好。有关autorelease pools can be found here 的更多信息。另请注意,如果您正在为 Mac OS X 10.5 及更高版本构建,您可以告诉 Xcode 在启用垃圾收集的情况下构建,从而完全忽略保留/释放/自动释放。

【讨论】:

这是错误的。在显示的任何一个示例中都无需发送 someObject release 或 autorlease。【参考方案5】:

Joshua (#6591) - Mac OS X 10.5 中的垃圾收集功能看起来很酷,但不适用于 iPhone(或者如果您希望您的应用在 Mac OS X 10.5 之前的版本上运行)。

另外,如果你正在编写一个库或可能被重用的东西,使用 GC 模式会锁定任何使用代码的人也使用 GC 模式,所以据我了解,任何试图编写可广泛重用的代码的人都倾向于去手动管理内存。

【讨论】:

完全可以编写一个同时支持GC和引用计数的混合框架。【参考方案6】:

与以往一样,当人们开始尝试重新编写参考资料时,他们几乎总是会出错或提供不完整的描述。

Apple 在Memory Management Programming Guide for Cocoa 中提供了对 Cocoa 内存管理系统的完整描述,在其末尾有对Memory Management Rules 的简短而准确的总结。

【讨论】:

试试:developer.apple.com/documentation/Cocoa/Conceptual/MemoryMgmt/… 总结规则:developer.apple.com/documentation/Cocoa/Conceptual/MemoryMgmt/… 其实这是一个更好的单页摘要:developer.apple.com/mac/library/documentation/Cocoa/Conceptual/…【参考方案7】:

除了您可能想考虑花 50 美元购买 Hillegass 书外,我不会添加保留/释放的具体内容,但我强烈建议您在开发应用程序的早期就开始使用 Instruments 工具(甚至是你的第一个!)。为此,运行-> 使用性能工具开始。我将从 Leaks 开始,它只是众多可用工具中的一种,但它有助于在您忘记释放时向您展示。您将看到多少信息不再令人生畏。但是请查看本教程以快速入门:COCOA TUTORIAL: FIXING MEMORY LEAKS WITH INSTRUMENTS

实际上尝试强制泄漏可能是学习如何防止泄漏的更好方法!祝你好运;)

【讨论】:

【参考方案8】:

Matt Dillard wrote:

return [[s autorelease] release];

自动释放不会保留对象。 Autorelease 只是将它放入队列中,以便稍后发布。你不想在那儿有发布声明。

【讨论】:

【参考方案9】:

我平时收藏的Cocoa内存管理文章:

cocoa memory management

【讨论】:

【参考方案10】:

iDeveloperTV Network 提供免费的截屏视频

Memory Management in Objective-C

【讨论】:

不幸的是,这个链接现在是 404。【参考方案11】:

NilObject 的回答是一个好的开始。以下是与手动内存管理有关的一些补充信息(iPhone 需要)。

如果您亲自alloc/init 一个对象,它的引用计数为 1。当不再需要它时,您负责清理它,方法是调用 [foo release][foo autorelease]。 release 立即清理它,而 autorelease 将对象添加到自动释放池中,稍后将自动释放它。

autorelease 主要用于当您有一个需要返回相关对象的方法时(因此您不能手动释放它,否则您将返回一个 nil 对象)但您没有也不想坚持。

如果你获取了一个没有调用 alloc/init 来获取的对象——例如:

foo = [NSString stringWithString:@"hello"];

但是你想挂在这个对象上,你需要调用[foo retain]。否则,它可能会得到autoreleased,而您将保留一个零引用(就像在上面的stringWithString 示例中一样)。当您不再需要它时,请致电[foo release]

【讨论】:

【参考方案12】:

上面的答案清楚地重述了文档所说的内容;大多数新人遇到的问题是无证案件。例如:

Autorelease:文档说它会在“未来某个时候”触发发布。什么时候?!基本上,在您将代码退出回系统事件循环之前,您可以依靠该对象。系统可以在当前事件循环之后的任何时间释放对象。 (我认为马特早些时候说过。)

静态字符串NSString *foo = @"bar"; -- 你必须保留还是释放它?不。怎么样

-(void)getBar 
    return @"bar";

...

NSString *foo = [self getBar]; // still no need to retain or release

创建规则:如果您创建了它,您就拥有它,并且应该发布它。

一般来说,新 Cocoa 程序员搞砸的方式是不了解哪些例程返回带有 retainCount > 0 的对象。

这是来自Very Simple Rules For Memory Management In Cocoa的sn-p:

保留计数规则

在给定块内,-copy、-alloc 和 -retain 的使用应与 -release 和 -autorelease 的使用相同。 使用便利构造函数(例如 NSString 的 stringWithString)创建的对象被认为是自动释放的。 实现 -dealloc 方法以释放您拥有的实例变量

第一个项目符号说:如果您调用了alloc(或new fooCopy),则需要对该对象调用release。

第二个要点说:如果你使用了一个方便的构造函数并且你需要对象在周围徘徊(就像稍后要绘制的图像一样),你需要保留(然后稍后释放)它。

第三个应该是不言自明的。

【讨论】:

“自动发布:文档说它会在“未来的某个时间点”触发发布。什么时候?!”文档在这一点上很清楚:“自动释放只是意味着“稍后发送发布消息”(有关稍后的一些定义 - 请参阅“自动释放池”)。完全取决于自动释放池堆栈... ... "系统可以在当前事件循环之后的任何时间释放对象。"这使得系统听起来比实际更不具有确定性...... ... NSString foo = [self getBar]; // 仍然不需要保留或释放 这是错误的。调用 getBar 的人不知道实现细节,因此如果他们想在当前范围之外使用它,*应该保留/释放(通常通过访问器)。 “非常简单的 Cocoa 内存管理规则”文章在几个方面已经过时了——尤其是“使用便利构造函数创建的对象(例如 NSString 的 stringWithString)被认为是自动释放的。”是不对的——它只是“不属于收件人”。【参考方案13】:

还有很多关于 cocoadev 的好信息:

MemoryManagement RulesOfThumb

【讨论】:

【参考方案14】:

正如一些人已经提到的,Apple 的Intro to Memory Management 是迄今为止最好的起点。

我还没有看到提到的一个有用的链接是Practical Memory Management。如果您通读它们,您会在 Apple 文档的中间找到它,但值得直接链接。这是内存管理规则的精彩执行摘要,其中包含示例和常见错误(基本上这里的其他答案试图解释,但不是很好)。

【讨论】:

以上是关于使用 Cocoa 和 Objective-C 理解引用计数的主要内容,如果未能解决你的问题,请参考以下文章

从 C++ 和 Objective-C++ 链接到 Cocoa 框架

在代码中使用核心数据 - Objective-C/Cocoa

Objective-C/Cocoa:获取关键代码

Objective-C / Cocoa:上传图像、工作内存和存储

什么是 Cocoa,和 Objective-C 有什么关系

什么是用于 Objective-C 和 Cocoa 编程的好的 IRC 频道 [关闭]