translation:adc:cocoa:foundation:introduction_to_dynamically_loading_code:0700_loadingbundles

バンドルの読み込み

NSBundleクラスはCocoaバンドルを読み込むためのメソッドを提供します。 本章ではCocoaアプリケーションにおけるバンドル読み込みの基本を解説します。 また、Cocoaアプリケーションで非Cocoaバンドルを読み込む方法についても扱います。 この記事はアプリケーションで読込み可能バンドルを使う全ての開発者に関係します。

NSBundleクラスはCocoaバンドルから実行可能コードとリソースを読み込むためのメソッドを提供します。 NSBundleは読み込み、インクルード、Mach-Oローダdyldとのやり取り、そしてObjective-CシンボルのObjective-Cランタイムへの追加手続きを全て扱います。

NSBundleを使ったコード以外のリソースの読み込みに関する情報は、Resource Programming Guideをご覧ください。

Cocoaバンドルの読み込みは5つの基本的な手順からなります:

  1. バンドルの配置
  2. そのバンドルを表すNSBundleオブジェクトの生成
  3. バンドルの実行コードの読み込み
  4. バンドルの主要クラスの問い合わせ
  5. 主要クラスオブジェクトのインスタンス化

以後の節でこれら各ステップの詳細を解説します。

アプリケーションは任意の場所からバンドルを読み込むことが出来ますが、バンドルが既定の場所に保管されていれば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を確保し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];

殆どのアプリケーションで、バンドル読込みの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;
}

コードの動作解説:

  1. instances配列は見つかったバンドルの主要クラスからインスタンス化された全てのオブジェクトを持ちます。このオブジェクトは分かり易さの為にメソッド内で定義していますが、通常はコントローラクラスのインスタンス変数になるでしょう。
  2. loadAllBundlesメソッドはallBundlesメソッドを呼び出し、拡張子.bundleで終わる全てのファイルを検索します。allBundlesメソッドは読み込み可能バンドル用に定められたパスの全て(アプリケーションバンドル内と、ユーザー用、ローカル環境用、ネットワーク用のLibraryディレクトリ)を走査します。
  3. 返された各パスごとにNSBundleオブジェクトが生成されます。.bundle拡張子のファイルが実際には正しいバンドルでなかった場合、NSBundleはnilを返し、以後の処理をスキップします。
  4. この行では現在のバンドルの主要クラスを取得します。principalClassの呼び出しは、処理の最初に暗黙的なコード読み込みを行います。
  5. 最後に、主要クラスをインスタンス化します。initnilが返ってこなければ、その新しいインスタンスがinstances配列に追加されます。プラグインの仕組みを持つアプリケーションを書く場合(いくつかの既知である読み込み可能バンドルを使うアプリケーションとは全く異なり)、主要クラスのインスタンス生成の前に、ある種の正当性チェックをプラグインに対して行うべきです。

場合によっては、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つ1つに対応する関数ポインタ型の定義
  2. 関数ポインタのインスタンス変数
  3. CFBundleRefインスタンス変数
  4. 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

実装が何をしているかというと…

  1. cfBundleインスタンス変数を、アプリケーションのplug-insディレクトリにあるバンドルを指すURLで初期化しています。バンドルはディスク上のどこにでも存在出来ますが、plug-insディレクトリは内蔵する読み込み可能バンドルの置き場所としてまさに典型的です。
  2. メソッドが呼ばれると、メソッドに紐付けられた関数ポインタが遅延初期化されます。CFBundleGetFunctionPointerForNameの呼び出しは関数ポインタ検索の前に、暗黙的にバンドルの実行可能コードを読み込みます。
  3. 読み込んだ関数の戻り値を返します。
  • translation/adc/cocoa/foundation/introduction_to_dynamically_loading_code/0700_loadingbundles.txt
  • 最終更新: 2020-12-03 16:11
  • by Decomo