🍎

Interface Builderを使わずにMacアプリケーション作成 - nib検証篇 5

2020/09/20に公開

NSApplicationMain()関数を使わないで本当に大丈夫か

大丈夫なはずがない(13/4/24追記)

NSApplicationMain()関数を使わずにアプリケーションを起動させること自体は可能だ。しかしその行為には全く意味が無い。Interface Builderによって作成するxibファイル、そしてコンパイルして作成されるnibファイル、これらはウインドウやボタンの配置だけを定義しているものではない。

nibはソースコードでもプログラムでもなく、インスタンスそのもののアーカイブである。これをNSApplicationMain()関数内で解凍し使える状態に戻している。自身で作成したコントローラサブクラスやDelegateクラスのインスタンスも、このタイミングで解凍されるのだ。

ということは、これらのクラスのインスタンスを使用する場合、必然的にnibのロード作業(解凍)を行なうことになる。さんざんNSApplicationMain()関数を使わない方法を模索しても、このnibロードは回避しようがないのだ。


以下の内容は掲載当時、nibの必要性についての検証が足りなかった時点のものである。NSApp変数のメモリ参照などを検証したので、検証内容はそのまま載せておく。

NSAppのRetain count

NSApplicationMain()関数は、NSApplicationのクラスメソッド+ sharedApplicationを呼び出していると説明されている。このとき作成されたオブジェクトは、NSAppグローバル変数に代入され保持される。アプリケーションの起動直後にこのNSAppのRetain countを調べてみた。

アプリケーション起動直後の処理ということで、今回はAppDelegateクラス(Xcodeが最初に作るもの)を使い、- applicationDidFinishLaunching:内で調べる。delegateの接続はInterface Builder上で行った。

AppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSLog(@"applicationDidFinishLaunching:");
    NSLog(@"Retain count: %d", (int)CFGetRetainCount((__bridge CFTypeRef)NSApp));
}

ARC利用時に- retainCountメソッドは使えないが、NSAppCore Foundationオブジェクトにキャストすることで同様の操作が行える。なお__bridge修飾子は、使ってもRetain countは変わらず所有権も移行しない。

typecast
(__bridge CFTypeRef)NSApp // Core Foundation オブジェクトとしてキャスト

NSApplicationMain()関数を使用

main()
int main(int argc, char *argv[])
{
    return NSApplicationMain(argc, (const char **) argv);
}

この状態で実行してみよう。まずはNSApplicationMain()関数を使ったとき。

console
2013-04-22 12:17:58.996 withoutIB[4341:303] applicationDidFinishLaunching:
2013-04-22 12:17:58.997 withoutIB[4341:303] Retain count: 3

カウントは3と表示された。

書き換えmain()関数を使用

main()
int main(int argc, char *argv[])
{
    @autoreleasepool {
        [NSApplication sharedApplication];
        [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];
        [NSApp run];
    }
    return EXIT_SUCCESS;
}

続いて書き換えmain()関数。ここで前回との違いとして、@autoreleasepoolブロックで囲っている。これは他の資料やAppleリファレンスを読み考えたことなのだが、[NSApp run]でイベントループ内にAuto release poolが用意されるものの(リファレンス参照)、外には何も無いままだったからだ。

console
2013-04-22 12:18:26.209 withoutIB[4375:303] applicationDidFinishLaunching:
2013-04-22 12:18:26.210 withoutIB[4375:303] Retain count: 3

結果は同じく3となった。

NSLogを増やしRetain countを追う

せっかく1行ずつ分解できるので、さらに細かく調べてみる。

main()
int main(int argc, char *argv[])
{
    @autoreleasepool {
        NSLog(@"Retain count: %d", (int) CFGetRetainCount((__bridge CFTypeRef) NSApp));
        [NSApplication sharedApplication];
        NSLog(@"Retain count: %d", (int) CFGetRetainCount((__bridge CFTypeRef) NSApp));
        [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];
        NSLog(@"Retain count: %d", (int) CFGetRetainCount((__bridge CFTypeRef) NSApp));
        [NSApp run];
    }
    return EXIT_SUCCESS;
}

1行ずつNSAppのRetain countを確認するためNSLogを挿入した。ただし上記のまま実行するとSIGKILLが発生してしまう。[NSApplication sharedApplication]より前で空のNSApp変数を参照しているからで、これは期待通りの動作だ。

問題の行は削除するとして、再度実行。

console
2013-04-22 12:26:57.434 withoutIB[4711:303] Retain count: 1
2013-04-22 12:26:57.505 withoutIB[4711:303] Retain count: 2
2013-04-22 12:26:57.593 withoutIB[4711:303] applicationDidFinishLaunching:
2013-04-22 12:26:57.594 withoutIB[4711:303] Retain count: 3

わかりやすい数字が出た。+ sharedApplication、これは+ allocに相当する処理を行なっているので1増加。nibを読み込み画面上に描画する時点で保持し1増加、最後にAppDelegateクラスによって保持され1増加、合計3である。

NSApplicationのクラスリファレンスに書かれた概要の通りとなったようだ。

課題

検証でnibは今後必須と発覚した。そんなnibの読み込みについて、裏ではどういう処理をしているのか。

次回NSApplicationMain()関数の処理の詳細について検証する。

リンク

Discussion