Cocoa创建插件体系结构

Posted by CoderLeonidas on March 18, 2020

Cocoa创建插件体系结构

许多应用程序都受益于插件体系结构,它可以在不更改主要应用程序代码的情况下使用新特性扩展应用程序。本节描述如何为Cocoa应用程序创建插件架构。

插件体系结构建设

实现插件体系结构的第一步是确定希望插件采用何种形式。有关如何决定要使用哪种机制的指南,请参阅Plug-in Architecture Design

在Cocoa体系中通常支持以下三种选择:

  • 插件实现正式协议
  • 插件实现一些非正式协议的方法
  • 插件继承自抽象或具体的基类

在这三种情况下,您为插件开发人员提供了一个标准接口,他们编写插件主体类(principal classes)来匹配接口。最方便的方法是提供接口作为插件开发人员链接的framework。

下面几节将描述如何发布这三种不同类型的插件接口。

发布正式协议的插件接口

要使用您定义的插件协议,插件开发人员只需要包含协议定义的头文件。发布接口的最简单方法是简单地分发这个头文件。

图形筛选器插件的协议头可能类似于清单1

Listing 1 Formal protocol for a plug-in architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
   MyGreatImageApp
   Graphics Filter Interface version 0
   MyAppBitmapGraphicsFiltering.h
*/
 
#import <Cocoa/Cocoa.h>
 
@protocol MyGreatImageAppBitmapGraphicsFiltering
 
// Returns the version of the interface you're implementing.
// Return 0 here or future versions may look for features you don't have!
- (unsigned)interfaceVersion;
 
// Returns what to display in the Filter menu.
- (NSString *)menuItemString;
 
// The main worker bee: filters the bitmap and returns a modified version.
- (NSBitmapImageRep *)filteredImageRep:(NSBitmapImageRep *)imageRep;
 
// Returns the window controller for the settings configuration window.
- (NSWindowController *)configurationWindowController;
 
@end

请注意,这个示例包含一个版本方法,因此应用程序可以识别正在使用的接口的版本。如果您只是分发一个头文件,这是确保应用程序的未来版本避免向旧插件发送消息的最佳方法,因为旧插件无法处理这些消息。您还可以查询插件,了解它响应什么消息,如Validating Plug-ins中所述。

发布非正式协议的插件接口

为插件体系结构使用非正式协议比使用正式协议稍微复杂一些。因为插件开发人员可以选择要实现哪些方法,所以您的应用程序必须在使用方法之前检查插件是否实际实现了方法。如果有些方法是可选的,有些方法是必需的,请确保在接口文档中明确说明这一点。

您可以分发单个头文件来定义非正式协议,就像使用正式协议一样。清单2是清单1的另一种实现,它使用非正式协议而不是正式协议,有效地使一些方法成为可选的。与大多数非正式协议一样,这个协议是作为NSObject上的一个分类实现的。

Listing 2 Informal protocol for a plug-in architecture

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
/*
   MyGreatImageApp
   Graphics Filter Interface version 0
   MyAppBitmapGraphicsFiltering.h
*/
 
#import <Cocoa/Cocoa.h>
 
@interface NSObject(MyGreatImageAppBitmapGraphicsFiltering)
 
// REQUIRED
// Returns the version of the interface you're implementing.
// Return 0 here or future versions may look for features you don't have!
- (unsigned)interfaceVersion;
 
// OPTIONAL
// Returns what to display in the Filter menu. Defaults to the plug-in
// filename without the extension.
- (NSString *)menuItemString;
 
// REQUIRED
// The main worker bee: filters the bitmap and returns a modified version.
- (NSBitmapImageRep *)filteredImageRep:(NSBitmapImageRep *)imageRep;
 
// OPTIONAL
// Returns the window controller for the settings configuration window.
// If this method is not implemented, no Settings option is provided.
- (NSWindowController *)configurationWindowController;
 
@end

发布基类的插件接口

如果您想要为所有插件提供共享功能,您就需要为插件主体类提供一个基类。例如,OS X屏幕保护程序接口作为一个framework提供,其中包含ScreenSaverView基类,它处理屏幕保护程序插件的大量管理细节。要分发基类,最好的解决方案是将其打包为供插件开发人员链接的framework。

清单3显示了一个假设的嵌入插件体系结构的接口,清单4显示了它的实现。接口不是作为协议提供的,而是作为提供一些功能的基类提供的,在本例中是一个相当简单的NSView子类,它返回版本信息、维护包含数据源的URL并绘制白色背景。注意,该类包含三个reserved的指针。这允许基类在不改变对象大小的情况下添加新成员数据,从而允许主应用程序开发人员在不创建二进制不兼容(binary incompatibilities)的情况下添加特性。

Listing 3 Base class interface for a plug-in architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Cocoa/Cocoa.h>
 
@interface MyAppEmbeddingView : NSView
{
@private
    NSURL *_URL;
    void *_reserved1;
    void *_reserved2;
    void *_reserved3;
}
 
- (id)initWithFrame:(NSRect)frameRect URL:(NSURL)URL;
- (unsigned)interfaceVersion;
- (NSURL *)URL;
- (void)setURL:(NSURL *)URL;
 
@end

Listing 4 Base class implementation for a plug-in architecture

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
#import "MyAppEmbeddingView.h"
 
@implementation MyAppEmbeddingView
 
- (id)initWithFrame:(NSRect)frameRect URL:(NSURL)URL
{
    self = [self initWithFrame:frameRect];
    [self setURL:URL];
    return self
}
 
- (unsigned)interfaceVersion
{
    return 0;
}
 
- (void)drawRect:(NSRect)rect
{
    NSEraseRect(rect);
}
 
- (NSURL *)URL
{
    return _URL;
}
 
- (void)setURL:(NSURL *)URL
{
    [URL retain];
    [_URL release];
    _URL = URL;
}
 
@end

完成基类之后,应该将编译后的实现与头文件打包在一个framework中,供插件开发人员使用。要构建framework,请使用Xcode的Cocoa framework项目模板。确保在“Target设置”面板的“Build Phases > Headers”部分中指定您想要的私有和公共头文件。有关构建框架的更多信息,请参见在Xcode帮助中的“Creating Frameworks and Libraries”。

加载插件

要使用插件,您的应用程序需要经历这个过程:

  • 1 在标准位置查找可用的插件
  • 2 加载每个插件的可执行代码
  • 3 验证每个插件与插件接口的一致性
  • 4 实例化有效插件

查找、加载和实例化插件的具体细节与Loading Cocoa Bundles with NSBundle.所描述的相同。插件体系结构的附加步骤是验证每个插件是否符合发布给插件开发人员使用的接口。

根据插件体系结构是使用正式协议、非正式协议还是基类,验证略有不同。

验证插件

对于正式协议,您可以查询类来查看它是否实现了协议。为了安全起见,您还应该执行一个事实检查(reality check),以确定它确实实现了它声称要实现的方法。清单5显示了一个方法的实现,该方法检查插件的主体类是否符合清单1中定义的MyGreatImageAppBitmapFiltering协议,另外还检查它是否实现了所有必需的实例方法。

Listing 5 Plug-in validation (formal protocol)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (BOOL)plugInClassIsValid:(Class)plugInClass
{
    if([plugInClass conformsToProtocol:@protocol(MyGreatImageAppBitmapFiltering)])
    {
        if([plugInClass instancesRespondToSelector:
                        @selector(interfaceVersion)] &&
           [plugInClass instancesRespondToSelector:
                        @selector(menuItemString)] &&
           [plugInClass instancesRespondToSelector:
                        @selector(filteredImageRep)] &&
           [plugInClass instancesRespondToSelector:
                        @selector(configurationWindowController])
        {
            return YES;
        }
    }
 
    return NO;
}

对于非正式协议,您必须查询该类以查看它实现了哪些方法。通常,主应用程序开发人员将某些方法定义为必需的,而将其他方法定义为可选的,因此验证方法应该正确区分这两种方法。清单6给出了插件验证方法的非正式版本的实现。该方法确保插件实现了清单2中给出的MyGreatImageAppBitmapFiltering非正式版本中所需的所有方法。您可以检查可选方法,因为它们在应用程序的其他地方是需要的。

Listing 6 Plug-in validation (informal protocol)

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)plugInClassIsValid:(Class)plugInClass
{
    if([plugInClass instancesRespondToSelector:
                    @selector(interfaceVersion)] &&
       [plugInClass instancesRespondToSelector:
                    @selector(filteredImageRep)])
    {
        return YES;
    }
 
    return NO;
}

从基类继承的插件是最容易验证的。您只需查询插件的主体类,查看它是否是基类的子类,如清单7所示。

Listing 7 Plug-in validation (base class)

1
2
3
4
5
6
7
8
9
10
- (BOOL)plugInClassIsValid:(Class)plugInClass
{
    if([plugInClass isSubclassOfClass:[MyAppEmbeddingView class]])
    {
        return YES;
    }
 
    return NO;
}

加载插件:示例代码

清单8是清单1的稍微修改后的版本,它在添加插件之前先对其进行验证,由边注// Validation表示。 有关代码如何工作的完整说明,请参见原始版本。

Listing 8 Implementation for plug-in loading methods

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
85
NSString *ext = @"plugin";
NSString *appSupportSubpath = @"Application Support/KillerApp/PlugIns";
 
// ...
 
- (void)loadAllPlugins
{
    NSMutableArray *instances;
    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]];
 
    pathEnum = [bundlePaths objectEnumerator];
    while(currPath = [pathEnum nextObject])
    {
        currBundle = [NSBundle bundleWithPath:currPath];
        if(currBundle)
        {
            currPrincipalClass = [currBundle principalClass];
            if(currPrincipalClass &&
               [self plugInClassIsValid:currPrincipalClass])  // Validation
            {
                currInstance = [[currPrincipalClass alloc] init];
                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;
},

参考文章

Creating Plug-in Architectures