【问题标题】:Subtle memory management issue in Objective-CObjective-C 中微妙的内存管理问题
【发布时间】:2010-08-18 23:48:32
【问题描述】:

我最近(经过数小时的调试)在一个 Objective-C iPad 应用程序中发现了一个段错误。归结起来,我有一个对象 TOP,它拥有 MIDDLE,它拥有 BOTTOM。 MIDDLE 和 BOTTOM 的保留计数为 1。MIDDLE 将 BOTTOM 传递给 TOP 中的一个方法,该方法最终释放了 MIDDLE,从而导致 BOTTOM 被释放并释放。当 TOP 中的相同方法继续使用 BOTTOM 时,它出现了段错误。 (请注意,为了简单起见,我在描述中省略了多层间接,但这让调试变得很麻烦。)

发生的事情有名字吗?有没有我可以遵循的模式来防止它在未来发生?为什么运行时不通过使用 [self retain][self release] 包装方法(或以相同方式或两者兼而有之)来保留调用堆栈上的对象?

编辑:

需要明确的是,当 TOP 释放 MIDDLE 时,它会将指针设置为 nil。永远不会通过无效指针访问 MIDDLE。

编辑 2: 我应该首先发布实际代码。这基本上就是我所拥有的:

// also known as TOP
@interface MyAppDelegate : NSObject <UIApplicationDelegate> {
  UIViewController* controller;
}
@end

@implementation MyAppDelegate
- (void)displayDoc:(Document*)doc {
  DocController* c = [[DocController alloc] initWithNibName:@"DocController" bundle:nil doc:doc];
  [controller release];
  // controller was previously an instance of HomeController
  controller = c;
}

- (void)displayBookmark:(Bookmark*)bookmark {
  [self displayDoc:bookmark.document];
  [controller setPage:bookmark.page];
}

- (void)dealloc {
  [controller release];
  [super dealloc]
}
@end


// also known as MIDDLE
@interface HomeController : UIViewController {
}
@end

@implementation HomeController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  Bookmark* b = ...; // pull out of existing data structure, not created here
  MyAppDelegate* app = ...;
  [app displayBookmark:b];
}
@end


// also known as BOTTOM
@interface Bookmark : NSObject {
}
@property NSString* name;
// etc.
@end

【问题讨论】:

    标签: objective-c memory-management


    【解决方案1】:

    对于一般问题,除了它的工作方式之外,没有很好的答案。非垃圾回收的 Objective-C 是一种显式内存管理的语言。这意味着由您来处理对象所有权语义。有一些惯例可以帮助您做到这一点,但最终您必须注意谁拥有什么。

    现在,您可以争辩说,仅在调用堆栈上的存在就是一种事实上的所有权,并且以这种方式设计语言当然是可能的。然而,在实践中,这将需要大量多余的保留和释放,这在所有精心设计的代码中都是不必要的,并且只会在极少数边缘情况下提供似是而非的安全缓冲区。它可能还会使很多真正的内存管理错误更难找到。

    当然,你有权不同意这种分析,但你和我在这件事上都没有任何发言权。

    在您描述的特定情况下,在我看来,问题在于您在从 MIDDLE 传递 BOTTOM 时没有正确定义所有权语义。有两种可能:

    1. MIDDLE 返回一个弱引用,随后可能会消失。在这种情况下,在做任何可能改变 BOTTOM 状态的事情之前,由 TOP 到 retain 决定——在 MIDDLE 上调用 release 肯定属于该类别。
    2. MIDDLE 将临时所有权授予调用方,在自动释放池耗尽之前,引用预计将保持良好状态:return [[BOTTOM retain] autorelease]; 在这种情况下,之后可以安全地使用release MIDDLE。

    在 Objective-C 的立场上,这两种方法都是有效的方法,并且都可以工作。你只需要明确地决定你正在使用哪个并采取相应的行动。您的错误源于执行第一个但期望它的行为类似于第二个。

    【讨论】:

    • MIDDLE 调用 TOP,而不是相反。请参阅我的编辑。 MIDDLE 在所有权方面处于中间位置。
    • @Adam 好的,但它对论点没有什么实质性的影响。基本问题仍然相同,您只是将其包装在更不透明的控制流中。如果是我,我想我不会声称这是对我有利的一点。
    • 它确实有所作为,因为返回一个对象(并转移所有权)不同于将一个对象作为参数传递并仅授予临时所有权。我从来没有声称我做的事情是正确的;毕竟,我承认有一个错误。
    【解决方案2】:

    运行时无法合理地为您保留和释放每个参数的原因有很多。

    1. 人们已经抱怨 Objective-C 的效率低下。除了方法的实际代码之外,对于每个方法调用都强制使用 arity*2 消息将是疯狂的。

    2. 不能假定任意对象响应retainrelease。这是符合NSObject 的类所独有的。

    3. 参数可以而且经常是反弹。为此,运行时必须跟踪参数的初始值。

    4. 有理由不想要这种行为,这似乎至少与想要它的理由一样有效(本质上,它可以在你不想要的情况下简化设计去遵循内存管理合同的麻烦)。可能有一种方法会故意释放一个参数并在其位置分配一个新参数(沿着init 方法的行),对于大型对象,这可能会导致更大的内存占用。这在 iPhone 上尤其麻烦,因为它是retainrelease 的主要平台。

    【讨论】:

    • #2 是一件事让我一直绊倒。我觉得很奇怪,Objective C 中没有唯一的根类。
    • @Adam Crume:这是 Objective-C 继承的 Smalltalkism。
    【解决方案3】:

    在 TOP 中,您可能有一个指向 MIDDLE 的指针。释放指向 MIDDLE 的指针时,是否将指针设置为 NULL,以便知道内存可能无效?

    【讨论】:

    • 这不是问题,问题是释放 MIDDLE 会导致释放 BOTTOM。 TOP 用 MIDDLE 结束了,但还想用 BOTTOM。
    【解决方案4】:

    我将其称为倒立的 Münchhausen (http://en.wikipedia.org/wiki/Baron_M%C3%BCnchhausen)。

    还有:为什么你的 TOP 不保留它愿意拥有的对象(至少在它正在使用它的时候)?因为它属于另一个对象,属于 TOP?展开这张图片:每个对象都以某种方式归应用程序所有。那么为什么要做所有这些保留发布游戏呢?数一数就够了!还是不行?

    问候

    【讨论】:

    • 我的解决方法是在 TOP 的方法中保留/释放 BOTTOM。您关于 TOP 暂时拥有该对象的问题与我关于运行时为什么不自动保留方法参数的问题一致。
    【解决方案5】:

    “为什么运行时不保留方法参数”?没有人告诉我你的名字是 Yossaran:http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmObjectOwnership.html#//apple_ref/doc/uid/20000043-1044135

    问候

    【讨论】:

    • 我已经读过了。我熟悉 Objective-C 中内存管理的工作原理;我只是碰巧认为它坏了。
    【解决方案6】:

    您是否真的尝试在 TOP 方法的开头(在释放 MIDDLE 之前)保留 BOTTOM?

    如果是这样,并且如果这不起作用,请检查您是否只是释放 MIDDLE 和 BOTTOM,或者您是否正在强制其中一个或两个立即释放(通过直接调用 dealloc 而不是释放)。虽然您只是在 MIDDLE 的 dealloc 方法中释放 BOTTOM,但如果还有另一个对象保留它,则系统不应将其释放。

    我希望我没有错过重点 - 如果是这样,也许您应该通过发布一些代码来澄清您的问题,包括从 TOP 到 BOTTOM 的发布链。

    虽然我仍然会尝试解决这个问题,但为什么不推迟 MIDDLE 的发布,直到 TOP 不再需要 BOTTOM?

    【讨论】:

      【解决方案7】:

      有没有我可以遵循的模式来防止它在未来发生?

      是的。您的 TOP 对象需要声明 BOTTOM 对象的所有权。您拥有通过调用“alloc”、“new”或“copy”获得的任何对象。我们可能会假设您没有通过调用这三个方法之一获得对 BOTTOM 对象的引用,这意味着如果您希望 BOTTOM 继续存在,则需要对其调用“保留”。

      您不得不花时间追踪您的错误,这很糟糕,但从它的声音来看,您的对象的行为完全符合惯例。

      【讨论】:

      • BOTTOM 被传递到方法中(但不存储)。你是说我应该保留方法参数吗?
      • TOP 通过方法调用获得对 BOTTOM 的引用,对吧?如果 TOP 想要保留该对象,则需要保留它。
      猜你喜欢
      • 2011-05-11
      • 2011-03-13
      • 1970-01-01
      • 1970-01-01
      • 2015-12-09
      • 2015-01-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多