Bundle的加载

Posted by CoderLeonidas on March 18, 2020

Bundle的加载

NSBundle类提供了加载Cocoa Bundle的方法。本节描述在Cocoa应用程序中加载bundle的基础知识。还讨论了从Cocoa应用程序加载非Cocoa 的Bundle。本材料适用于任何在其应用程序中使用可加载Bundle的开发人员。

使用NSBundle加载Cocoa bundle

NSBundle类提供了从Cocoa bundle中加载可执行代码和资源的方法。它处理加载的所有细节,包括与Mach-O加载器dyld的交互,以及将Objective-C符号加载到Objective-C运行时。

有关使用NSBundle加载非代码资源的信息,请参阅Resource Programming Guide

加载Cocoa bundle包含五个基本步骤:

  • 找到bundle。
  • 创建一个NSBundle对象来表示bundle。
  • 加载bundle的可执行代码。
  • 查询bundle以获取其主体类。
  • 实例化主体类的对象

下面的小节将详细介绍这些步骤。

找到bundle

您的应用程序可以从任何位置加载bundle,但是如果它们存储在标准位置,则可以使用Cocoa提供的函数和方法轻松地找到它们。

与应用程序打包在一起的可加载的bundle通常包含在Contents/PlugIns中的应用程序bundle内。 要检索主应用程序bundle PlugIns目录,请使用NSBundle的builtInPlugInsPath方法。

这段代码展示了如何使用NSBundle来检索一个应用程序的PlugIns目录,它可以被命名为PlugIns或Plug-ins(前者取代了后者)

1
2
3
4
5
NSBundle *appBundle;
NSString *plugInsPath;
 
appBundle = [NSBundle mainBundle];
plugInsPath = [appBundle builtInPlugInsPath];

尽管它不是标准位置,但是您可以通过将可加载的bundle存储在应用程序bundle的Res​​ources目录中来获得一些便利。 然后,您可以使用NSBundle的pathsForResourcesOfType:inDirectory:方法查找它们。 此代码段在应用程序的Resources/PlugIns目录中找到所有扩展名为.bundle的文件和目录:

1
2
3
4
5
NSBundle *appBundle;
NSArray *bundlePaths;
 
appBundle = [NSBundle mainBundle];
bundlePaths = [appBundle pathsForResourcesOfType:@"bundle"  inDirectory:@"PlugIns"];            

您的应用程序可能还会在多个domain的“Library”目录内的应用程序支持目录中支持bundle:

  • 用户指定(〜/Library)
  • 系统范围(/Library)
  • 网络(/Network/Library)

要搜索这些目录和其他标准目录,请使用NSSearchPathForDirectoriesInDomains函数。

这个代码片段为您的应用程序创建了一个搜索路径数组来查找bundle,然后您可以搜索各个插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSString *appSupportSubpath = @"Application Support/KillerApp/PlugIns";
NSArray *librarySearchPaths;
NSEnumerator *searchPathEnum;
NSString *currPath;
NSMutableArray *bundleSearchPaths = [NSMutableArray array];
 
// Find Library directories in all domains except /System
librarySearchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask - NSSystemDomainMask, YES);
 
// Copy each discovered path into an array after adding
// the Application Support/KillerApp/PlugIns subpath
searchPathEnum = [librarySearchPaths objectEnumerator];
while(currPath = [searchPathEnum nextObject])
{
    [bundleSearchPaths addObject: [currPath stringByAppendingPathComponent:appSupportSubpath]];
}

创建一个NSBundle对象来表示bundle

要为你想加载的bundle创建一个NSBundle对象,要么分配一个对象并使用initWithPath: initializer,要么使用便利的创建方法bundleWithPath:。如果bundle的实例已经存在,那么这两个方法都会返回现有的实例,而不是创建一个新的实例。

此代码片段检索位于fullPath的bundle:

1
2
3
4
5
NSString *fullPath; // Assume this exists.
NSBundle *bundle;
 
bundle = [NSBundle bundleWithPath:fullPath];

加载bundle的可执行代码

要加载一个bundle的可执行代码,使用NSBundle的load方法。如果加载成功或代码已经加载,则此方法返回YES,否则返回NO

此代码片段在fullPath加载bundle的代码:

1
2
3
4
5
6
NSString *fullPath; // Assume this exists.
NSBundle *bundle;
 
bundle = [NSBundle bundleWithPath:fullPath];
[bundle load];

查询bundle以获取其主体类

每个Cocoa bundle都包含一个principal类的代码,它通常充当一个应用程序到一个bundle的入口点。你用NSBundle的principalClass方法来检索一个bundle的主体类,如果它还没有被加载,这个方法会加载这个bundle。此代码片段检索位于fullPath的bundle的主体类:

1
2
3
4
5
6
NSString *fullPath; // Assume this exists.
NSBundle *bundle;
Class principalClass;
 
bundle = [NSBundle bundleWithPath:fullPath];
principalClass = [bundle principalClass];

您还可以使用classNamed:方法按名称检索类对象。这段代码从fullPath包中检索KillerAppController类:

1
2
3
4
5
6
7
NSString *fullPath; // Assume this exists.
NSBundle *bundle;
Class someClass;
 
bundle = [NSBundle bundleWithPath:fullPath];
someClass = [bundle classNamed:@"KillerAppController"];

实例化主体类的对象

一旦您从可加载的bundle中检索了主体类,您通常会创建类的一个实例来在您的应用程序中使用。(如果类通过类方法提供其所有功能,则不需要此步骤。)为此,您可以像使用任何类名一样使用类变量。

1
2
3
4
5
6
7
8
9
NSString *fullPath; // Assume this exists.
NSBundle *bundle;
Class principalClass;
id instance;
 
bundle = [NSBundle bundleWithPath:fullPath];
principalClass = [bundle principalClass];
instance = [[principalClass alloc] init];

加载Cocoa bundle:示例代码

在大多数应用程序中,包加载的五个步骤发生在启动过程中,在启动过程中搜索并加载插件。清单1显示了一对方法的实现,这些方法定位bundle、创建NSBundle对象、加载它们的代码,并查找和实例化每个发现的bundle的主体类。清单后面有一个解释。

Listing 1 Method implementations for loading bundles from various locations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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方法只是枚举可加载bundle的所有标准路径(在应用程序包以及用户、本地和网络库目录中)。
  • 对于每个返回的路径,都会创建一个NSBundle对象。如果扩展名为.bundle的文件实际上不是一个有效的bundle, NSBundle返回nil,剩下的迭代被跳过。
  • 这一行检索当前包的主体类。调用principalClass隐式地先加载代码。
  • 最后,该方法实例化主体类。只要init不返回nil,就会将新实例添加到实例数组中。如果您正在编写具有插件体系结构的应用程序(而不是具有几个已知可加载包的应用程序),那么您应该在创建principal类的实例之前对插件执行某种验证。

使用CFBundle加载非cocoa包

在某些情况下,您可能需要从Cocoa应用程序中加载非Cocoa bundle。您可以使用Core Foundation中的CFBundle例程来加载非cocoa bundle:

  • CFBundleCreate来创建CFBundle对象;
  • CFBundleLoadExecutable加载包的可执行代码;
  • CFBundleGetFunctionPointerForName来查找加载的例程的地址。

有关这些方法和CFBundle提供的其他方法的更多信息,请参阅Core Foundation编程主题Bundle Programming Guide

要更干净地将代码与Cocoa应用程序集成,您可以编写一个包装类来封装通过CFBundle查找的数据和函数指针。

清单2显示了CFBundle的Cocoa包装类的接口,清单3显示了它的实现。每个列表后面都有一个解释。

Listing 2 Loading and using code from a non-Cocoa bundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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

该接口包含四个元素:

  • 1 函数指针的类型定义,Bundle中每个函数的类型定义
  • 2 函数指针实例变量
  • 3 CFBundleRef实例变量
  • 4 包装C函数的Objective-C方法

Listing 3 Loading and using code from a non-Cocoa bundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#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 使用指向应用程序插件目录中的bundle的URL初始化cfBundle实例变量。bundle可以驻留在磁盘上的任何位置;插件目录只是内置可加载的 bundle的典型位置。
  • 2 当调用该方法时,惰性地初始化与该方法关联的函数指针。在查找函数指针之前,对CFBundleGetFunctionPointerForName的调用将隐式加载包的可执行代码。
  • 3 返回已加载函数返回的值。

参考文章

Loading Bundles