【问题标题】:Best practice for NSAutoreleasePool in callbacks from another threadNSAutoreleasePool 在来自另一个线程的回调中的最佳实践
【发布时间】:2012-01-15 11:43:43
【问题描述】:

我有一个 C++ 库,我想将它公开为一个 Objective-C 框架,因此它对 Objective-C 开发人员来说会更容易使用。在整理 C++ 库时,我遇到了一个在处理自动释放对象和线程方面的特殊问题。

这个库的一个特点是开发者可以注册一个“记录器”来接收通知消息作为来自库的回调。来自库的通知使用 C++ 类型并且是从另一个 (POSIX) 线程接收的,所以我创建了一个私有 C++ 包装类来处理这个问题:它接收回调,将 char* 参数转换为 NSString,并将其传递给到用户提供的 Objective-C 记录器实例。这一切都很好,看起来像这样:

// Is called from the C++ library from another posix thread
void ObjCLoggerWrapper::LogMessage(const char *message)
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  // Pass string to the user-provided Objective-C instance called "Logger"
  [Logger logMessage:[[[NSString alloc] initWithUTF8String:message] autorelease]];
  [pool release];
}

作为用户回调的示例,我编写了这个简单的方法来收集用户类实例中 NSString 成员 m_text 中的所有日志记录(将在其他地方使用,但这无关紧要)。

-(void) logMessage: (NSString*)message
{
  @synchronized(self)
  {
    m_text = [m_text stringByAppendingFormat:@"%04d: %@\r\n", m_lineno++, message];
  }
}

到目前为止一切顺利。或者我是这么想的。但这是牛肉:

用户回调方法中所有自动释放的对象都将属于包装器的 NSAutoreleasePool,因此在回调完成时会被释放。

哎呀!这意味着我的 m_text 字符串,由 stringByAppendingFormat 消息隐式创建为自动释放对象,将在 logMessage 完成时释放并成为僵尸。下次访问时,代码会崩溃。当然,用户当然不会期望这一点。在意识到发生了什么之前,我自己不得不挠头几次。

所以我的问题是:当从另一个线程对用户代码进行回调时,我们应该如何处理自动释放的对象?

我看到了几种可能的选择。没有一个是完美的,谷歌也没有帮助(因此这个问题)。

  1. 告诉用户“不要在你的回调代码中创建自动释放对象”。不好:此类对象通常是非自愿创建的,例如通过 stringByAppendingFormat 和大量其他框架方法。除了后来发生的难以调试的崩溃之外,没有任何警告。
  2. 没有 NSAutoreleasePool。如果用户尝试创建自动释放的对象,缺少一个将导致警告。绝对不漂亮,但会以稳健的方式提醒用户注意问题。并且用户可以“只是”添加一个他自己的 NSAutoreleasePool 来解决这个问题。但再说一遍:不漂亮。
  3. 没有 NSAutoreleasePool 并使用 performSelectorOnMainThread 在主线程上运行回调。任何新的自动释放对象都会在主线程的池中结束。我认为这是安全的,但欢迎 cmets - 例如,是否可以始终在主线程上执行回调?这种方法需要在包装器中进行更精细的编码,以避免线程死锁并等待结果,但到目前为止,这是我的首选。

明确一点:重写我自己的包装器是没有问题的。我的主要优先事项是为 Objective-C 框架的用户创建一个可以流畅无缝地工作的解决方案。谢谢!

【问题讨论】:

  • m_text 是一个 ivar,对吧?我不清楚为什么有人会认为m_text = <autoreleased object> 是安全的。我不会。如果我真的需要做这样的事情,我会使用类似于m_text = [...[m_text autorelease] ... retain]; 的东西
  • IOW,我认为这不是你的问题。自动释放范围适用于您的 fn defn (-logMessage:) 中的对象和您返回的值,否则假设它们是持久的是错误的。
  • 是的,它是 ivar。你是对的,它可能不是“我的问题”,但这似乎是一个容易犯的错误,如果可能的话,我真的更愿意保护我的框架的用户免受这种错误的影响。您的建议类似于我的选项 2,即“不要创建 NSAutoreleasePool 并让用户以这种方式发现他的错误”。毕竟这可能是最好的选择。
  • 这不是您的自动释放方法的问题。问题是所写的 logMessage 从根本上是有缺陷的。每当调用之间封闭的自动释放池耗尽时,它都会失败。主运行循环(模优化)在每次事件循环旋转时都会耗尽其自动释放池,所以这会在那里失败,不是吗?我错过了什么吗?
  • 嗯,是的,我想你是对的!作为一名新手 Objective-C 开发人员,我实际上并没有想到这一点 :-) 我仍然关心保护像我这样的菜鸟不会无意中伤害自己,如果有一种更可控的方式来发出“你”的信号,那就太好了又做错了!”而不是间接地将他们的对象变成僵尸。但同样,如果它们无论如何都会被主运行循环释放,那么我想我的池并没有增加任何伤害。

标签: objective-c multithreading cocoa autorelease nsautoreleasepool


【解决方案1】:

这不是您的自动释放方法的问题。你的方法看起来不错。 问题是logMessage,因为所写的从根本上是有缺陷的。每当调用之间封闭的自动释放池耗尽时,它都会失败。主运行循环(模优化)在每次事件循环旋转时都会耗尽其自动释放池,因此在这种情况下,它会在那里失败。

几个 FWIW:

[Logger logMessage:[[[NSString alloc] initWithUTF8String:message] autorelease]];

可以写

[Logger logMessage:[NSString stringWithUTF8String:message]];

并且[pool drain][pool release] 更受欢迎

【讨论】:

  • 关于 [泳池排水] 的要点。我以为这是 10.6 的功能,但现在看到它是在 10.4 中引入的。这已经很老了,但我猜在调用它之前它可能仍然应该经过一个 respondsToSelector 测试——在编写框架时比抱歉更安全。
【解决方案2】:

如果不确切知道您的库是做什么的,就很难推荐一种方法。但是,调用 performSelectorOnMainThread 是一种相当安全的策略。鉴于您不希望日志消息需要立即执行任何操作,因此等待主线程执行您的回调应该没问题。

【讨论】:

  • 还有一个用户提供的接口,带有两个回调方法,用于自定义保存/加载数据。这些需要同步,因此包装器必须等待它们完成。顺便说一下,该库是 EQATEC Analytics 的 iOS/MacOS 监视器。
猜你喜欢
  • 1970-01-01
  • 2020-10-20
  • 2011-02-14
  • 2012-03-23
  • 2010-10-14
  • 1970-01-01
  • 2019-03-16
  • 2014-06-14
  • 1970-01-01
相关资源
最近更新 更多