February 9, 2012

ARCとNSWindowでハマった

Xcode 4の“Convert to Objective-C ARC…”を使って「へへっ、楽チンだぜ」って言ってたらハマった時のお話です。主にNSWindowまわりで。

NSWindowにはisReleasedWhenClosedっていうプロパティがありますよね。これがYESだとウインドウが閉じられると同時に自動的に解放されるわけですが、これがデフォルトでYESなんですよね(NSPanelではNOがデフォルトですが)。しかしARC的にはこの解放を検知できないので、問題が起きる可能性があります。

論よりコード、例えば以下のようなコードがあったとします。

@implementation WindowToggleController {
    NSWindow *_window;
}

- (void)toggleWindow:(id)sender {
    if (_window) {
        [_window close];
        _window = nil; // ポイントA
    }
    else {
        _window = [[NSWindow alloc] initWithContentRect:NSMakeRect(100, 100, 200, 200)
                                                 styleMask:NSTitledWindowMask
                                                   backing:NSBackingStoreBuffered
                                                     defer:YES];
        [_window orderFront:self];
    }
}

@end

このコードは、- toggleWindow:が呼び出されるたびにウインドウを開いたり閉じたりすることを期待していますが、実際にはコード中のポイントAでクラッシュします。直前の[_window close]でウインドウが既に解放されているので、nilの代入によって再び解放処理が走って過解放となるわけですね。

じゃあ既に解放されているならnilの代入をやめてしまえばいいのかというと、そういうわけでもありません。ウインドウが解放されたのに変数がnil化されなくなり、上記のコードでは既に解放されたウインドウを再び閉じようとしてしまい、やはりクラッシュします。

というわけで、上のようなコードを動かしたいなら、事前に- [NSWindow setReleasedWhenClosed:NO]をしておきましょう。そうすればウインドウを閉じただけでは解放されなくなり、適切にnilを代入できるようになります。もしくはウインドウを閉じたい局面で(- [NSWindow close]を呼ばずに)直接nilを代入するのも一つの手かもしれません。

© Shun Takebayashi