从另一个线程的回调中使用NSAutoreleasePool的最佳实践

3
我有一个C++库,希望将其作为Objective-C框架公开,以便Objective-C开发人员更容易使用。在封装C++库时,我遇到了一个特定的问题,涉及autorelease对象和线程。
该库的一个特性是,开发人员可以注册“日志记录器”来接收来自库的回调通知消息。来自库的通知使用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在主线程上运行回调。任何新的自动释放对象都将出现在主线程的池中。我认为这是安全的,但欢迎评论-例如,回调总是可以在主线程上执行吗?这种方法需要包装器中更精细的编码以避免线程死锁并等待结果,但到目前为止,这是我首选的选择。
只是为了让它清楚:重写我的包装器没有问题。我的主要优先事项是创建一个对Objective-C框架的用户平稳无缝工作的解决方案。谢谢!

m_text 是一个实例变量,对吧?我不明白为什么有人会认为 m_text = <autoreleased object> 是安全的。我不会这样做。如果我真的需要做类似的事情,我会使用类似于 m_text = [...[m_text autorelease] ... retain]; 的方法。 - smparkes
是的,这是一个ivar。你说得对,这可能不是“我的问题”,但这似乎是一个容易犯的错误,我真的很希望能够尽可能地保护我的框架用户免受此类错误的影响。你的建议类似于我的第二个选项,即“不创建NSAutoreleasePool并让用户通过这种方式发现他的错误”。毕竟,这可能是最好的选择。 - Richard Flamsholt
这不是你的自动释放池方法存在问题。问题在于logMessage本身就有根本性缺陷。每次调用之间如果有封闭的自动释放池被排空,它就会失败。主运行循环(除了优化)在每个事件循环周期都要排空自动释放池,所以这也同样会严重失败,是吗?我有什么遗漏的吗? - smparkes
嗯,是的,我想你是对的!作为一个新手Objective-C开发者,我实际上没有考虑过这一点 :-) 我仍然担心像我这样的新手会不经意地伤害自己,如果有一种更受控制的方式来发出“你做错了!”的信号,那将是很好的。但是再次说一遍,如果它们将被主运行循环释放,那么我想我的池并没有增加任何危害。 - Richard Flamsholt
我现在明白了,谢谢!我会更深入地研究GCD,但它是MacOS 10.6的功能,而我们希望这个框架能够在尽可能旧的MacOS版本上工作。如果您将您的输入撰写成答案,我会将其标记为已接受(除非其他人想出一个“解决”此问题的绝妙主意)。 - Richard Flamsholt
显示剩余2条评论
2个回答

2
这并不是你的自动释放池方法存在问题。你的方法看起来是正确的。 问题在于代码中的logMessage写法本质上有缺陷。一旦在调用之间关闭了封闭自动释放池,它将会失效。主运行循环(模数优化)在每次事件循环时都要排空其自动释放池,所以在这种情况下,这个方法同样会失败。 几点提示:
[Logger logMessage:[[[NSString alloc] initWithUTF8String:message] autorelease]];

可以编写

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

相比于[pool release][pool drain]更受青睐。


关于[池排放]你说的很有道理。我曾认为这是10.6版功能,但现在我看到它是在10.4中引入的。虽然那已经很早了,但在编写框架时还是应该进行一个respondsToSelector测试再调用它,以防万一。 - Richard Flamsholt

1

如果不知道你的库确切的功能,很难推荐一种方法。然而,调用performSelectorOnMainThread是一个相当安全的策略。考虑到你不希望日志消息需要立即执行任何操作,等待主线程执行回调应该是可以的。


还有另一个用户提供的界面,其中包含两个回调方法,用于自定义数据的保存/加载。这些需要是同步的,因此封装器必须等待它们完成。顺便说一下,该库是EQATEC Analytics的iOS / MacOS监视器。 - Richard Flamsholt

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