调用 -retainCount 被认为是有害的。

39

或者说,为什么我在暑假期间没有使用retainCount

本文旨在征求关于那个臭名昭著的方法retainCount的详细描述,以整合SO上流传的相关信息。

  1. 基础知识:不使用retainCount的官方理由是什么?是否存在任何情况下它可能会有用?应该使用什么代替?**请随意发表评论。

  2. 历史/解释性内容:如果retainCount不打算使用,为什么苹果提供这个方法在NSObject协议中?苹果的代码是否依赖于retainCount来实现某种目的?如果是这样,为什么它不被隐藏在某个地方?

  3. 更深入的了解:对象之所以可能具有与用户代码所假定的不同的保留计数,有什么原因?您能否举出标准过程的示例***,这些过程可能导致框架代码产生这样的差异?是否有任何已知情况,其中保留计数始终与新用户所期望的不同?

  4. 关于retainCount还有其他值得提及的事情吗?


* 刚接触Objective-C和Cocoa的编程人员经常会苦恼或至少误解引用计数方案。教程中可能会提到保留计数,这些(根据这些解释)在调用retainalloccopy等时增加1,在调用release(和将来某个时间点调用autorelease)时减少1。

一个初学的Cocoa黑客,Kris,可能会很容易地想到检查对象的保留计数在解决一些内存问题时会很有用。然后,retainCount是每个对象上都可用的一个方法!Kris调用了几个对象上的retainCount,这个太高,那个太低,到底怎么回事?于是Kris在SO上发布了一篇文章,“我的内存管理有什么问题?”然后一大群粗体、大号字母降临,说“不要这样做!你不能依赖于结果。”这是好的,但我们勇敢的编码者可能需要更深入的解释。
我希望这能成为一个常见问题解答(FAQ),一个好的信息文章/讲座页面,任何愿意写的专家都可以指向新的Cocoa头部,当他们想知道retainCount时。
*** 在虚拟代码中;显然,普通公众无法访问苹果的实际代码。

我发现了这个相当新的问题:https://dev59.com/4G445IYBdhLWcg3w6eXz 和 Dave DeLong 的非常有用的回答,但是正如我所说,我希望创建一个集中的位置来获取retainCount信息(并学习一些东西!),_特别是_讨论retainCount存在的原因以及其无用性的示例。不用说,如果您认为这是一个无用的重复问题,请投票关闭,我会删除它! - jscs
1
因此:“您可以在类中覆盖此方法以实现自己的引用计数方案。” - SteAp
@Stefan:这是一个很好的观点。我希望你稍后能够将其扩展成答案(即使简短)。 - jscs
@Bavarious:谢谢。虽然这似乎是显而易见的举动。 - jscs
请也查看这个链接 - https://dev59.com/4G445IYBdhLWcg3w6eXz#4636477 - Aditya Aggarwal
显示剩余6条评论
2个回答

29
基础知识:为什么不建议使用retainCount?
自动释放池管理是最明显的原因,你无法确定由retainCount表示的引用有多少个在本地或外部的(在第二个线程中或在另一个线程的本地池中)自动释放池中。
此外,一些人在引用计数和自动释放池在基本层面上的工作方面存在问题。他们会编写程序而没有考虑正确的引用计数,或者没有学习好引用计数。这使得他们的程序非常难以调试、测试和改进 - 这也是一个非常耗时的矫正过程。
不建议使用它(在客户端级别)的原因有两个:
1. 值可能因很多原因而变化。仅仅线程就足以让你永远不相信它。
2. 你仍然必须实现正确的引用计数。retainCount永远也无法从不平衡的引用计数中拯救你。
有没有任何情况下可能有用呢?
实际上,如果您编写自己的分配器或引用计数方案,或者如果您的对象驻留在一个线程上并且您可以访问它存在的所有autorelease池,那么您确实可以以有意义的方式使用它。这也意味着您不会与任何外部API共享它。模拟此的简单方法是创建一个具有一个线程、零自动释放池和用“正常”方式进行引用计数的程序。除了学术目的外,您很少需要解决此问题/编写此程序。
作为调试工具:你可以使用它来验证保留计数是否异常高。如果采用这种方法,请注意实现差异(在本帖中引用了一些实现差异),不要依赖它。甚至不要把测试提交到你的版本控制库。
这在极其罕见的情况下可能是有用的诊断工具。它可用于检测:
1. 过度保留:如果分配具有保留计数不平衡,则不会显示出泄漏,如果该分配可由您的程序访问,则不会显示出泄漏。
2. 被许多其他对象引用的对象:这个问题的一个例子是一个(可变的)共享资源或集合,它在多线程上下文中运行——对这个资源/集合的频繁访问或更改可能会导致程序执行的重大瓶颈。
3. 自动释放级别:自动释放,自动释放池和保留/自动释放周期都有成本。如果您需要最小化或减少内存使用量和/或增长,则可以使用此方法来检测过度的情况。
从下面与Bavarious的评论中得知:高值也可能表示无效的分配(dealloc'd实例)。这完全是一些实现的细节,同样不能在生产代码中使用。当启用zombies时,向此分配发送消息将导致错误。
应该做什么?
如果你不负责返回self的内存(即,你没有编写一个分配器),那么请放弃它——它是无用的。
你必须学习正确的引用计数。
为了更好地理解释放和自动释放的使用方式,请设置一些断点,并了解它们在哪些情况下使用,等等。你仍然必须学会正确使用引用计数,但这可以辅助你理解为什么它是无用的。
NSLog(@"%qu", [@"MyString" retainCount]); 
// Logs: 1152921504606846975

你认为还有什么关于 retainCount 值得一提的吗?

retainCount 对于调试并无用处。学会使用泄露和僵尸对象分析工具,经常使用它们——即使在掌握了引用计数之后也是如此。


更新:bbum 发布了一篇名为 retainCount is useless 的文章。该文章对为什么在绝大多数情况下 -retainCount 无用进行了全面讨论。


2
一个注意点:-retainCount 永远不会变成零。 - user557219
3
谢谢。这句话的意思是:“仅仅因为线程存在,就足以让人不信任它。”它提供了一个很好的见解。 - jscs
3
不返回零只是一种实现细节,因为返回零需要使释放的对象仍然可被访问 (这可能还需要确保在同一地址上不再分配其他对象)。 - bbum
4
定义上,当保留计数器(retain count)转变为零时,支持该对象的内存会被释放。之后对该对象的任何消息(包括-retainCount)都会导致不确定的行为。为了解决这个问题,你必须使得在释放后仍能向对象发送有效的消息。因此,你永远不能重用任何地址来创建新的对象,因为这会违反上述协议(除非你还添加了每个分配都有相关联UUID的要求)。 - bbum
3
你当然可以选择这条路,但这样做会完全脱离 Cocoa/iOS 运行时的实际情况。不过这样做也是完全支持的。你可以定义自己的根类,并且可以发明任何你想要的分配模式。只是不要将你的对象传递到框架 API 中去!(实际上这是一个非常有趣的思维练习——我多年来创建了一些根类来探索不同的模型。) - bbum
显示剩余17条评论

1

一般的经验法则是,如果你使用这种方法,你最好非常确定自己在做什么。如果你用它来调试内存泄漏,那么你就错了;如果你用它来查看对象的情况,那么你也错了。

我只有在做共享对象缓存时才使用过它,并发现它很有用。在这种情况下,我等到 retainCount 等于 1,然后释放它,因为我知道没有其他东西再持有它了。在垃圾回收环境中,这显然行不通,而且有更好的方法可以解决这个问题。但这仍然是我见过的唯一“有效”的用例,而且不是很多人会做的事情。


您所描述的使用案例是一个非常糟糕的例子 - 即使创建缓存,您也不应以任何方式使用retainCount。缓存不应关心它包含的任何对象是否有其他用户使用 - 它应该在感觉适合的时候释放旧对象 - 如果有其他人持有它们,那就这样。 - Michal
Michal,重点是尽快清除过期对象。对于任何未来的事情,我会使用NSCache来实现这个目的,但当时它还不存在。不过,正如我所说,我永远不会向任何人推荐这种做法。 - Joshua Weinberg
使用保留计数来进行缓存是错误的。缓存不应该关心是否有其他人仍在使用缓存对象,当一个对象最近没有被请求时,缓存应该将其从自身中清除。在那里使用retainCount来缓存一个对象以延长其生命周期和“尽快清除过期对象”的目的是两码事。 - Michal
@Michal:我很感激你对这种技术在实际应用中的关注,但是我认为这个答案(以及你的评论)对retainCount进行了有益的讨论。因此,我已经给这个答案点了赞(将其带回到0)。 - jscs
2
@Michal: 只要对象正在使用中(由保留计数>1表示),将其从缓存中驱逐不会释放任何内存。由于存在对象再次被使用的可能性,因此保留对该引用是明智的。如果对象从缓存中移除,然后在之前持有它的人释放先前实例之前重新缓存,则会存在多余的实例漂浮。 - jfortmann
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接