为啥这个简单的 NSWindow 创建代码会在 ARC 下关闭时触发自动释放池崩溃?

Posted

技术标签:

【中文标题】为啥这个简单的 NSWindow 创建代码会在 ARC 下关闭时触发自动释放池崩溃?【英文标题】:Why does this simple NSWindow creation code trigger an autorelease pool crash on shutdown under ARC?为什么这个简单的 NSWindow 创建代码会在 ARC 下关闭时触发自动释放池崩溃? 【发布时间】:2012-10-31 23:23:56 【问题描述】:

我遇到了关机时自动释放池崩溃的问题,我已将其简化为下面的小测试用例,它只是创建一个窗口然后将其关闭。如果 -fobjc-arc 标志被拿走,崩溃就会消失。在 OS X 10.8.2、Clang 4.1 (421.11.66) 上运行。我希望对 ARC 有更深入了解的人可以告诉我这里发生了什么 - 与僵尸对象一起运行表明它是 NSWindow 对象被释放了太多次,或者没有足够的保留,但是我以为 ARC 是用来处理这一切的?

堆栈跟踪是:

0   libobjc.A.dylib                 0x00007fff8fad4f5e objc_release + 14
1   libobjc.A.dylib                 0x00007fff8fad4230 (anonymous namespace)::AutoreleasePoolPage::pop(void*) + 464
2   com.apple.CoreFoundation        0x00007fff99d22342 _CFAutoreleasePoolPop + 34
3   com.apple.Foundation            0x00007fff936e84fa -[NSAutoreleasePool drain] + 154
4   com.apple.Foundation            0x00007fff936effa0 _NSAppleEventManagerGenericHandler + 125
5   com.apple.AE                    0x00007fff93a5ab48 aeDispatchAppleEvent(AEDesc const*, AEDesc*, unsigned int, unsigned char*) + 307
6   com.apple.AE                    0x00007fff93a5a9a9 dispatchEventAndSendReply(AEDesc const*, AEDesc*) + 37
7   com.apple.AE                    0x00007fff93a5a869 aeProcessAppleEvent + 318
8   com.apple.HIToolbox             0x00007fff8d0c18e9 AEProcessAppleEvent + 100
9   com.apple.AppKit                0x00007fff8e95c916 _DPSNextEvent + 1456
10  com.apple.AppKit                0x00007fff8e95bed2 -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] + 128
11  com.apple.AppKit                0x00007fff8e953283 -[NSApplication run] + 517
12  Test                            0x00000001070e1d68 main + 152 (Test.mm:31)
13  libdyld.dylib                   0x00007fff8e10c7e1 start + 1

而测试用例的代码是:

// Tested with `clang++ -fobjc-arc -g Test.mm -framework Cocoa -o Test && ./Test`

#import <Cocoa/Cocoa.h>

@interface MyApplication : NSApplication
@end
@implementation MyApplication
- (void) applicationDidFinishLaunching: (NSNotification *) note

    NSWindow * window = [[NSWindow alloc] initWithContentRect: NSMakeRect(100, 100, 100, 100)
                        styleMask: NSTitledWindowMask backing: NSBackingStoreBuffered defer: YES];

    [window close];

    [super stop: self];

@end

int main()

    @autoreleasepool
    
        const ProcessSerialNumber psn =  0, kCurrentProcess ;
        TransformProcessType(&psn, kProcessTransformToForegroundApplication);
        SetFrontProcess(&psn);

        [MyApplication sharedApplication];
        [NSApp setDelegate: NSApp];

        [NSApp run];
    

    return 0;

【问题讨论】:

【参考方案1】:

使用 Instruments 的 Zombies 配置文件显示 NSWindow 对象通过调用 close: 被放入自动释放池中。一旦applicationDidFinishLaunching: 完成并销毁 NSWindow 实例,ARC 就会正确地以零引用计数结束。但是,自动释放池仍然知道现已失效的 NSWindow 实例,然后在关闭时尝试释放它,从而导致崩溃。

在 ARC 下管理的自动释放对象似乎是个坏主意,除非自动释放池将对其对象的弱引用归零,而这里似乎没有这样做。

可以通过添加[window setReleasedWhenClosed: NO]; 告诉窗口不要在关闭时自动释放来防止该问题。

【讨论】:

【参考方案2】:

只有将新创建的对象分配给范围大于当前范围的变量时,ARC 才会保留它。否则,对象将被泄露。

在您的示例中,您正在通过调用 alloc 创建一个新的 NSWindow 实例,这会暂时将所有权转移到局部变量 window。由于该变量在方法结束时不再存在,ARC 必须插入一个release 调用以避免泄漏窗口实例。因此,该实例不再为任何事物所拥有,因此会自行释放。

要解决此问题,请使用 strong 语义声明 NSWindow 类型的属性,并将窗口实例传递给属性设置器方法(或直接将其分配给相应的实例变量 - 都可以)。

编辑

要明确的是,您需要做的是添加一个声明的属性(或至少一个实例变量)到MyApplication,例如

@interface MyApplication : NSApplication

@property (strong, nonatomic) NSWindow *window;

@end

然后,在您的applicationDidFinishLaunching 实现中,设置属性:

@implementation MyApplication

- (void) applicationDidFinishLaunching: (NSNotification *) note

    NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 100, 100)
                        styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:YES];

    self.window = window;

    ...


@end

【讨论】:

同意,如果我使用的窗口超出了applicationDidFinishLaunching 的范围,则该实例需要在更广泛的范围内进行强引用,但此示例只是尝试创建一个窗口然后关闭它。 NSWindow 对象在此测试用例中除此方法之外的任何其他地方均未使用。此外,将__strong 添加到window 的定义中没有任何区别,可能是因为默认情况下它已经存在。这有意义吗? 如果窗口实例不存在超出applicationDidFinishLaunching 的范围,您将永远无法使用它,因为它会立即被释放。为了使用该窗口,您必须确保它由您的应用程序中的某些东西拥有。局部变量window 只存在到方法范围的末尾。 ARC 然后向窗口实例发送release 消息。由于此时窗口没有所有者,它会自行释放。您需要将窗口存储在实例变量中,以防止其自行释放。 顺便说一句,您的代码在禁用 ARC 的情况下工作的原因是因为没有 ARC,窗口会在 applicationDidFinishLaunching 的末尾泄漏。此时,您对窗口实例的唯一引用是在局部变量window 中,该变量在方法结束时不再存在。由于您的代码不再知道窗口的地址,因此永远无法向其发送release 消息,因此永远无法解除分配。 是的,这个例子相当做作,只是为了说明崩溃,而不是真实世界窗口管理的例子!而且在禁用 ARC 的情况下,该窗口在技术上不会泄漏,[window close] 将其放入自动释放池中,因此当自动释放池结束时,它会在 main() 中释放。谢谢。【参考方案3】:

使用 ARC,您需要将属性用于假设可以维持更长时间的事物。

要抽象私有属性,请在 .m 文件中使用匿名类别。

更多信息在http://developer.apple.com/library/mac/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

这解决了它:

@interface MyApplication : NSApplication
@property NSWindow *window;

@end

@implementation MyApplication
- (void) applicationDidFinishLaunching: (NSNotification *) note

  self.window = [[NSWindow alloc] initWithContentRect: NSMakeRect(100, 100, 100, 100)
                                                  styleMask: NSTitledWindowMask backing: NSBackingStoreBuffered defer: YES];

  [self.window close];

  [super stop: self];

@end

int main()

  @autoreleasepool
  
    const ProcessSerialNumber psn =  0, kCurrentProcess ;
    TransformProcessType(&psn, kProcessTransformToForegroundApplication);
    SetFrontProcess(&psn);

    [MyApplication sharedApplication];
    [NSApp setDelegate: NSApp];

    [NSApp run];
  

  return 0;

希望对您有所帮助。

【讨论】:

这对于 ARC 应用程序来说是一个很好的一般性建议,但据我所知,这并不是本例中崩溃的原因。我想我最终想通了,请参阅我刚刚发布的答案以了解正在发生的事情的详细信息。谢谢。 好吧,那么至少给我们+1 Richard。 另外,根据您自己的评论 - 使用属性实际上也可以解决这个问题。并且被推荐,所以我仍然认为我的答案不那么老套。 我不明白,重点是这里的 NSWindow 实例是在 local 范围内,你是说我们需要在使用 ARC 时将本地范围内的实例存储在属性中?当该方法完成时,NSWindow 实例被正确释放,它并不意味着“停留更长时间”。问题是[window close] 会自动释放,如果删除该行,崩溃就会消失。这似乎确实是 ARC 代码和调用 autorelease 的非 ARC 代码之间的不良交互。 我不能说我是这些问题的专家。但在过去一年左右的时间里,我确实通过自己的经验阅读并注意到,您通常确实需要将以前不需要的东西存储为 ARC 的属性。

以上是关于为啥这个简单的 NSWindow 创建代码会在 ARC 下关闭时触发自动释放池崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

为啥添加约束会消除调整 NSWindow 大小的能力?

为啥设置 initialFirstResponder 没有效果?

为啥我的 NSWindow 第一次只接收 mouseOver 事件?

如何从 NSWindow 对象创建 NSTouchBar?

无法关闭窗口 - 为啥?

当 WebView 添加到 NSWindow 中的特定视图时,:hover 停止工作。我怎样才能解决这个问题?