🍎

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

2020/09/20に公開

結論 NSApplicationMain()関数は必要

今回の題から"Interface Builderを全く使わない"の__全く使わない__を取り下げた。誠に申し訳ないことに、記事執筆中の調査でNSApplicationMain()関数およびnibの必要性が確定してしまったからだ(本シリーズは調査と資料作成の同時進行を特徴としている)。ということで、今回から題を"Interface Builderを使わずにMacアプリケーション作成"に改め、テーマもnib検証篇とした。

NSApplicationMain()関数を使わずにアプリケーションを起動

NSApplicationMain()関数を使わずにアプリケーションを起動させること自体は可能だ。

main()
int main(int argc, char *argv[])
{
    [[NSApplication sharedApplication] run];
    return EXIT_SUCCESS;
}

究極的にはこのコードだけで「アプリケーションは起動」する。しかしその行為には全く意味が無い。Interface Builderによって作成するxibファイル、そしてコンパイルして作成されるnibファイル、これらはウインドウやボタンの配置だけを定義しているものではない。

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

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

Document-Based Applicationで困ることに

シングルウィンドウのアプリケーションの場合、本シリーズが目指していたInterface Builderを__全く使わない__作成は、達成できることがわかっている。困るのはDocument-Based Applicationの作成である。

Document-Based Applicationについては、いずれ今後の章で扱いたいと思っているが、Appleの提供するドキュメントアーキテクチャの主要クラスNSDocumentControllerNSWindowControllerではnibの使用を回避して実装するリスクがとても大きいようだ。

OSの設計とも密接なこのアーキテクチャを安易に模倣して実装してしまうと、その時は動いてもその後のOSのバージョンアップで毎度模倣し直す必要が出てくる。これは、当初の目的だったInterface Builderを使用しないメリットを大幅に上回るデメリットだ。

そのため、Interface Builderを「全く使わない」までは無理としても、使わずに効率的に進める方法についての研究は続けるとして本シリーズを継続していく。

nibのロードでは何が動いているか

今後もnibとうまく付き合っていくとして、その処理周りを調べてみたい。NSApplicationMain()関数の処理概要は公開されているが、実装コードを覗けない以上、ここを調べることができるのはXcodeに付属の高機能プロファイラ"Instruments"である。

InstrumentsのTime Profilerを使い取得したSample Listが以下のものである。かなり冗長になるので概要として編集した。

Sample_List_1
dyld _dyld_start
NSApplicationMain()関数開始
	+[NSBundle mainBundle]
	+[NSApplication initialize]
		-[NSUserDefaults(NSUserDefaults) initWithUser:]
	+[NSApplication sharedApplication]
		-[NSApplication init]
			-[NSApplication _registerWithDock]
	+[NSBundle(NSNibLoading) loadNibNamed:owner:]
		-[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:]
			-[NSWindowTemplate nibInstantiate]
				+[NSScreen screens]
				-[NSWindow initWithContentRect:styleMask:backing:defer:]
				-[NSWindow orderWindow:relativeTo:]
			    	-[NSView _drawRect:clip:]
	-[NSApplication run]
		-[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]
			CFRunLoopRunSpecific
        malloc_zone_free

先頭のdyldとはDynamic Loaderのことで、Foundationを筆頭とする全てのライブラリのロードに関わってくる根幹部分のことだ。その後、100以上の処理をdyldが約0.4秒でこなし、この辺りからNSApplicationMain()関数の表記が見られはじめた。見慣れたメソッド名も並ぶ中、内部ではかなりの処理を行なっている。NSBundleNSApplicationが起動の中心を担っているのは明らかで、nibの読み込みにはAppKitのクラスが動きまわる。

NSWindowTemplateクラスについてはnib検証篇 3でも触れたが、やはりnibをインスタンス化する際のクラスでありNSWindowそのものとの継承関係では無さそうだ。スクリーンの取得、ウインドウの描画、NSViewの描画という順番で行なっていることが確認できる。

上記一覧の最後となるCFRunLoopRunSpecificの辺りで約1秒だ。ここから約10秒後にmalloc_zone_freeに至る。この処理でメモリの一部解放が行われていた。起動後の安定化が行われているようだ。

Main nib file base nameを空欄にした場合の疑問

Main nib file base nameを空欄にする実験はnib検証篇 1で行った。(Project Name)-info.plistを書き換えるというものだ。

この時エラーを出したが、そのエラーの原因が分からないままだった。問題のログは次のものである。

console
2013-04-20 14:01:38.213 withoutIB[1399:303] Could not connect the action buttonPressed: to target of class NSApplication
2013-04-20 14:01:38.214 withoutIB[1399:303] Could not connect the action buttonPressed: to target of class NSApplication
2013-04-20 14:01:38.215 withoutIB[1399:303] Could not connect the action buttonPressed: to target of class NSApplication
2013-04-20 14:01:38.215 withoutIB[1399:303] Could not connect the action buttonPressed: to target of class NSApplication

NSApplicationMain()関数の動きを再確認

nib検証篇 1では、まだInstrumentsを用いた検証を行なっていなかった。上で活躍したInstrumentsをここでも使い、1で行った実験を再度試してみる。

Main nib file base nameに該当しないファイル名fooを指定した場合

nib検証篇 1の時は、コンソールにUnable to load nib file: foo, exitingと表示された。この部分の実態はこうなっている。

Sample_List_1
dyld _dyld_start
NSApplicationMain()関数開始
    +[NSBundle mainBundle]
    +[NSApplication initialize]
        -[NSUserDefaults(NSUserDefaults) initWithUser:]
    +[NSApplication sharedApplication]
        -[NSApplication init]
            -[NSApplication _registerWithDock]
    +[NSBundle(NSNibLoading) loadNibNamed:owner:]
        NSLogv
    __exit
NSApplicationMain()関数終了

ちょうどloadNibNamed:owner:の箇所で停止した。NSLogvFoundation Functionsにて定義されている、システムログとして出力するための関数のようだ。以下にAppleのリファレンスを引用する。

NSLogv
Logs an error message to the Apple System Log facility.

Main nib file base nameを空欄にした場合

次にMain nib file base nameを空欄にした場合。この時に表示されたログは本記事の冒頭で再掲している。これもInstrumentsのSample Listで該当箇所を探ってみた。

Sample_List_2
dyld _dyld_start
NSApplicationMain()関数開始
    +[NSBundle mainBundle]
    +[NSApplication initialize]
        -[NSUserDefaults(NSUserDefaults) initWithUser:]
    +[NSApplication sharedApplication]
        -[NSApplication init]
            -[NSApplication _registerWithDock]
    +[NSBundle(NSNibLoading) loadNibNamed:owner:]
        -[NSNibOutletConnector initWithCoder:]
        -[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:]
            -[NSWindowTemplate nibInstantiate]
                _LSExceptionsLoad
            -[NSNibControlConnector establishConnection]
                NSLogv
    -[NSApplication run]
        -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]
            CFRunLoopRunSpecific
            …以下略…

これはexitにならず起動を成功させている。ただし何も表示されない。

nibが見つからない場合にもウインドウの作成を試みている点が意外で、NSNibOutletConnectorなど前回は出てきていないクラスも動いている。_LSExceptionsLoadも前回絡んでいない。

ログのbuttonPressed:というメソッド自体はProfile内を検索しても見つからず残念ながら詳細は不明だが、Could not connect the actionというログとNSLogv直前のNSNibControlConnectorがこれに関連していると見られる。

課題

Appleが提供する各種アーキテクチャ、フレームワークは非常に洗練されており、ちょっとやそっとの検証程度じゃまったくぶれないほど、よく作りこまれていることがnib検証編を通じて明らかになった。

これから、MVCやDelegateといったCocoaアプリケーションの作成に欠かせない重要な要素を踏まえていくことになる中、まずはGUIアプリケーションの基本中の基本であるウインドウの表示について調べたい。

次回、テーマを変えウインドウ作成篇としてNSWindowに迫っていく。

続きます。

リンク

Discussion