バンドルの読み込み
NSBundleクラスはCocoaバンドルを読み込むためのメソッドを提供します。 本章ではCocoaアプリケーションにおけるバンドル読み込みの基本を解説します。 また、Cocoaアプリケーションで非Cocoaバンドルを読み込む方法についても扱います。 この記事はアプリケーションで読込み可能バンドルを使う全ての開発者に関係します。
NSBundleによるCocoaバンドルの読み込み
NSBundleクラスはCocoaバンドルから実行可能コードとリソースを読み込むためのメソッドを提供します。
NSBundleは読み込み、インクルード、Mach-Oローダdyld
とのやり取り、そしてObjective-CシンボルのObjective-Cランタイムへの追加手続きを全て扱います。
NSBundleを使ったコード以外のリソースの読み込みに関する情報は、Resource Programming Guideをご覧ください。
Cocoaバンドルの読み込みは5つの基本的な手順からなります:
- バンドルの配置
- そのバンドルを表すNSBundleオブジェクトの生成
- バンドルの実行コードの読み込み
- バンドルの主要クラスの問い合わせ
- 主要クラスオブジェクトのインスタンス化
以後の節でこれら各ステップの詳細を解説します。
バンドル所在地の取得
アプリケーションは任意の場所からバンドルを読み込むことが出来ますが、バンドルが既定の場所に保管されていればCocoa提供のメソッドと関数を使って簡単に見つけることが出来ます。
アプリケーションに内包される読込み可能バンドルは、典型的にアプリケーションバンドル内のContents/PlugIns
に置かれます。
メインアプリケーションバンドルのプラグインディレクトリを読み取るには、NSBundleのbuiltInPlugInsPath
メソッドを使用してください。
このコード片は、NSBundleを使ったアプリケーションのプラグインディレクトリの取得方法を示しています。ディレクトリ名はPlugIns
またはPlug-ins
で、前者が後者より優先されます:
NSBundle *appBundle; NSString *plugInsPath; appBundle = [NSBundle mainBundle]; plugInsPath = [appBundle builtInPlugInsPath];
既定の場所ではありませんが、アプリケーションバンドルのResources
ディレクトリに読み込み可能バンドルを配置することで、利便性を得られることもあります。
その場合、NSBundleのpathsForResourcesOfType:inDirectory:
メソッドを使って、それらを探すことが可能です。
次のコード片は、アプリケーションのResources/PlugIns
ディレクトリ内から、.bundle
拡張子を持つ全てのファイルとディレクトリを見つけます:
NSBundle *appBundle; NSArray *bundlePaths; appBundle = [NSBundle mainBundle]; bundlePaths = [appBundle pathsForResourcesOfType:@"bundle" inDirectory:@"PlugIns"];
また、アプリケーションは複数ドメインのLibrary
ディレクトリ(ユーザー固有:~/Library
、システム全体:/Library
、ネットワーク:/Network/Library
)に存在するApplication Support
ディレクトリ内のバンドルに対応したいかもしれません。
以下のコード片は、個々のプラグインを見つけることが可能となる、アプリケーションのバンドル検索対象パスの配列を作成します:
NSString *appSupportSubpath = @"Application Support/KillerApp/PlugIns"; NSArray *librarySearchPaths; NSEnumerator *searchPathEnum; NSString *currPath; NSMutableArray *bundleSearchPaths = [NSMutableArray array]; // 全ドメインのLibraryディレクトリを探す(/Systemは除く) librarySearchPaths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSAllDomainsMask - NSSystemDomainMask, YES); // 見つかった各パスの後ろにApplication Support/KillerApp/PlugIns // サブパスを連結し、配列にコピーする。 searchPathEnum = [librarySearchPaths objectEnumerator]; while(currPath = [searchPathEnum nextObject]) { [bundleSearchPaths addObject: [currPath stringByAppendingPathComponent:appSupportSubpath]]; }
NSBundleオブジェクトの生成
読み込みたいバンドルのNSBundleオブジェクトを生成するには、NSBundleを確保しinitWithPath:
イニシャライザを使うか、便利な生成メソッドbundleWithPath:
を使用します。
そのバンドルのインスタンスが既に存在していたら、両メソッドは新規インスタンスの代わりに既存のインスタンスを返します。
このコード片はfullPath
に置かれているバンドルを読み込みます:
NSString *fullPath; // ここにあると仮定 NSBundle *bundle; bundle = [NSBundle bundleWithPath:fullPath];
コードの読み込み
バンドルの実行可能コードを読み込むには、NSBundleのload
メソッドを使用します。
このメソッドは、読み込み成功時にYES
を返し、コードが読み込み済みだったりする場合はNO
を返します。
次のコード片はfullPath
のバンドルからコードを読み込みます:
NSString *fullPath; // ここにあると仮定 NSBundle *bundle; bundle = [NSBundle bundleWithPath:fullPath]; [bundle load];
主要クラスの読み取り
Cocoaバンドルは全て主要クラスにコードを持ちます。そして、典型的に主要クラスはバンドルに対するアプリケーションのエントリーポイントとなります。
NSBundleのprincipalClass
メソッドでバンドルの主要クラスを読み取ります。この時、バンドルがまだ読み込まれていなければ、読み込まれます。
次のコード片はfullPath
にあるバンドルの主要クラスを読み取ります:
NSString *fullPath; // ここにあると仮定 NSBundle *bundle; Class principalClass; bundle = [NSBundle bundleWithPath:fullPath]; principalClass = [bundle principalClass];
classNamed:
メソッドで名前からクラスオブジェクトを取得することも出来ます。
このコード片はfullPath
にあるバンドルからKillerAppControllerを取得します:
NSString *fullPath; // ここにあると仮定 NSBundle *bundle; Class someClass; bundle = [NSBundle bundleWithPath:fullPath]; someClass = [bundle classNamed:@"KillerAppController"];
主要クラスのインスタンス化
読込み可能バンドルから主要クラスを取得した後は、通常、アプリケーションで使用するクラスのインスタンスを生成します(クラスの全機能がクラスメソッドで提供される場合、この作業は必須ではありません)。
To do this, you use a Class
variable in the same way you would use any class name.
このコード片はfullPath
にあるバンドルの主要クラスを取得し、インスタンスを生成します:
NSString *fullPath; // ここにあると仮定 NSBundle *bundle; Class principalClass; id instance; bundle = [NSBundle bundleWithPath:fullPath]; principalClass = [bundle principalClass]; instance = [[principalClass alloc] init];
Cocoaバンドルの読み込み: コード例
殆どのアプリケーションで、バンドル読込みの5つのステップはスタートアップ工程の中で発生し、プラグイン検索や読込みが行われます。 リスト1は2つのメソッドの実装を示し、一方がバンドル所在地の取得、バンドルの探索、NSBundleオブジェクトの生成、バンドルのコードの読込みを行い、もう一方が見つかった各バンドルの主要クラスのインスタンス化を行います。解説はリストの後に続きます。
リスト1 様々な場所からバンドルを読み込むメソッドの実装
NSString *ext = @"bundle"; NSString *appSupportSubpath = @"Application Support/KillerApp/PlugIns"; // ... - (void)loadAllBundles { NSMutableArray *instances; // 1 NSMutableArray *bundlePaths; NSEnumerator *pathEnum; NSString *currPath; NSBundle *currBundle; Class currPrincipalClass; id currInstance; bundlePaths = [NSMutableArray array]; if(!instances) { instances = [[NSMutableArray alloc] init]; } [bundlePaths addObjectsFromArray:[self allBundles]]; // 2 pathEnum = [bundlePaths objectEnumerator]; while(currPath = [pathEnum nextObject]) { currBundle = [NSBundle bundleWithPath:currPath]; // 3 if(currBundle) { currPrincipalClass = [currBundle principalClass]; // 4 if(currPrincipalClass) { currInstance = [[currPrincipalClass alloc] init]; // 5 if(currInstance) { [instances addObject:[currInstance autorelease]]; } } } } } - (NSMutableArray *)allBundles { NSArray *librarySearchPaths; NSEnumerator *searchPathEnum; NSString *currPath; NSMutableArray *bundleSearchPaths = [NSMutableArray array]; NSMutableArray *allBundles = [NSMutableArray array]; librarySearchPaths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSAllDomainsMask - NSSystemDomainMask, YES); searchPathEnum = [librarySearchPaths objectEnumerator]; while(currPath = [searchPathEnum nextObject]) { [bundleSearchPaths addObject: [currPath stringByAppendingPathComponent:appSupportSubpath]]; } [bundleSearchPaths addObject: [[NSBundle mainBundle] builtInPlugInsPath]]; searchPathEnum = [bundleSearchPaths objectEnumerator]; while(currPath = [searchPathEnum nextObject]) { NSDirectoryEnumerator *bundleEnum; NSString *currBundlePath; bundleEnum = [[NSFileManager defaultManager] enumeratorAtPath:currPath]; if(bundleEnum) { while(currBundlePath = [bundleEnum nextObject]) { if([[currBundlePath pathExtension] isEqualToString:ext]) { [allBundles addObject:[currPath stringByAppendingPathComponent:currBundlePath]]; } } } } return allBundles; }
コードの動作解説:
instances
配列は見つかったバンドルの主要クラスからインスタンス化された全てのオブジェクトを持ちます。このオブジェクトは分かり易さの為にメソッド内で定義していますが、通常はコントローラクラスのインスタンス変数になるでしょう。loadAllBundles
メソッドはallBundles
メソッドを呼び出し、拡張子.bundle
で終わる全てのファイルを検索します。allBundles
メソッドは読み込み可能バンドル用に定められたパスの全て(アプリケーションバンドル内と、ユーザー用、ローカル環境用、ネットワーク用のLibrary
ディレクトリ)を走査します。- 返された各パスごとにNSBundleオブジェクトが生成されます。
.bundle
拡張子のファイルが実際には正しいバンドルでなかった場合、NSBundleはnil
を返し、以後の処理をスキップします。 - この行では現在のバンドルの主要クラスを取得します。
principalClass
の呼び出しは、処理の最初に暗黙的なコード読み込みを行います。 - 最後に、主要クラスをインスタンス化します。
init
でnil
が返ってこなければ、その新しいインスタンスがinstances
配列に追加されます。プラグインの仕組みを持つアプリケーションを書く場合(いくつかの既知である読み込み可能バンドルを使うアプリケーションとは全く異なり)、主要クラスのインスタンス生成の前に、ある種の正当性チェックをプラグインに対して行うべきです。
CFBundleによる非Cocoaバンドルの読み込み
場合によっては、Cocoaアプリケーションで非Cocoaバンドルの読み込みが必要になるかもしれません。
非Cocoaバンドルを読み込むためには、Core FoundationのCFBundleルーチンを使用します: CFBundleCreate
でCFBundleオブジェクトを生成し、CFBundleLoadExecutable
で当該バンドルの実行可能コードを読み込み、CFBundleGetFunctionPointerForName
で読み込んだルーチンのアドレスを探します。
これらメソッドやその他のCFBundleが提供するメソッドの詳細は、Core FoundationプログラミングトピックのBundle Programming Guideをご覧下さい。
コードをより奇麗にCocoaアプリケーションと統合するには、CFBundle経由で見つけたデータや関数のポインタをカプセル化するラッパークラスを書く事が出来ます。 リスト2はCFBundle向けのCocoaによるラッパークラスのインタフェースを示し、リスト3はその実装を示します。 解説は各リストの後にあります。
リスト2 非Cocoaバンドルの読み込み使用するコード
#import <CoreFoundation/CoreFoundation.h> typedef long (*DoSomethingPtr)(long); // 1 typedef void (*DoSomethingElsePtr)(void); @interface MyBundleWrapper : NSObject { DoSomethingPtr doSomething; // 2 DoSomethingElsePtr doSomethingElse; CFBundleRef cfBundle; // 3 } - (long)doSomething:(long)arg; // 4 - (void)doSomethingElse; @end
このインタフェースは4つの要素を持ちます:
- バンドル内の関数1つ1つに対応する関数ポインタ型の定義
- 関数ポインタのインスタンス変数
CFBundleRef
インスタンス変数- C関数をラップするObjective-Cメソッド
リスト3 非Cocoaバンドルの読み込み使用するコード
#import "MyBundleWrapper.h" @implementation MyBundleWrapper - (id)init { NSString *bundlePath; NSURL *bundleURL; self = [super init]; bundlePath = [[[NSBundle mainBundle] builtInPlugInsPath] // 1 stringByAppendingPathComponent:@"MyCFBundle.bundle"]; bundleURL = [NSURL fileURLWithPath:bundlePath]; cfBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)bundleURL); return self; } - (void)dealloc { CFRelease(cfBundle); } - (long)doSomething:(long)arg { if(!doSomething) // 2 { doSomething = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("DoSomething")); } return doSomething(arg); // 3 } - (void)doSomethingElse { if(!doSomethingElse) // 2 { doSomethingElse = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("DoSomethingElse")); } doSomethingElse(); // 3 } @end
実装が何をしているかというと…
cfBundle
インスタンス変数を、アプリケーションのplug-insディレクトリにあるバンドルを指すURLで初期化しています。バンドルはディスク上のどこにでも存在出来ますが、plug-insディレクトリは内蔵する読み込み可能バンドルの置き場所としてまさに典型的です。- メソッドが呼ばれると、メソッドに紐付けられた関数ポインタが遅延初期化されます。
CFBundleGetFunctionPointerForName
の呼び出しは関数ポインタ検索の前に、暗黙的にバンドルの実行可能コードを読み込みます。 - 読み込んだ関数の戻り値を返します。