在前文
深入淺出 Cocoa 之 Framework 中講解了 Framework,接下來講解 plugin。如果你對 Framework 還不太熟悉的話,請閱讀那篇文中,在本例中使用到了 framework,并在本文中沒有詳細講述其創建和使用過程。
為什么要引入插件?
我們知道編譯程序時,會連接相關 framework,通常我們所連接的框架是 Foundation 和 Application 框架。當程序啟動運行時,每個被連接到的 framework 都會被加載到該程序的 objc 運行時環境中。如果我們想向正在運行的程序加載新的 framework,那該怎么辦呢?答案之一就是使用 plugin 機制。cocoa 的 plugin 機制通常由 NSBundle 類來實現,而實現動態加載的功能由函數 objc_addClass 來完成。一般我們無需與 objc_addClass 這個函數打交道,我們使用 NSBundle 來完成絕大部分與 plugin 相關的工作。
plugin 機制能夠讓我們開發出高度模塊化,可定制以及可擴展的應用程序,并能夠讓第三方為該應用程序添加新特性。想必很多人都熟悉 Eclipse,Eclipse 的 plugin 機制就非常方便與強大。
NSBundle 簡介
束(bundle)是文件系統中的一個目錄結構,它將程序會使用到的資源打包在一起。這些資源可包括編譯好的代碼,nib文件,配置文件,圖像,聲音,本地化資源等等。束是 Mac OS X 的一個核心特性,應用程序,Framework,插件都是一個束,只是擴展名各異,如應用程序的擴展名為 .app;Framework 的擴展名是 .framework;插件的擴展名默認為 .bundle。
一個 plugin 就是一個 bundle(束),xcode 默認以 .bundle 為擴展名。通常我們使用我們自己定義的擴展名,以便與系統或其他人編寫的 plugin 區分開來。我們通過 NSBundle 來載入 bundle,并把其中經過編譯的類注冊到 objc 運行時中,然后我們就能在程序中使用這些類了;我們也可以使用 bundle 中的所有資源。
plugin 構架
我們可以通過多種途徑來實現一個 plugin:
1,定義一個 objc protocol,讓 plugin 遵守該 protocol;
2,定義一個基類,讓 plugin 繼承該基類;
3,定義一個 C 回調函數接口,讓 plugin 實現改回調函數;
4,使用 CFPlugIn 來創建 plugin 接口;
在今天的例子中,使用的是第二種情況,這種情況稍稍復雜一些,我們需創建一個 framework 供宿主程序(使用插件的程序)和 plugin 使用,該 framework 的主要職責是提供基類接口。
plugin 的存放目錄
通常 plugin 總是存放在以下三個位置:
1,應用程序名.app/Contents/Plug-ins 這是程序的開發者存放隨產品發布的插件的地方。
2,~/Library/Application Support/應用程序名/Plug-ins 用戶存放個人插件的地方。
3,/Library/Application Support/應用程序名/Plug-ins 系統中供全部用戶使用的插件。
在今天的例子中,使用的是第一種情況,即將插件存放在應用程序包中。
創建宿主程序
我們來創建一個名為 PluginDemo 的 cocoa application,該程序含有一個顯示已安裝 plugin 的 popup button 以及一個執行選中 plugin 的 button。

創建 framework
1,創建名為 PluginFramework 的 framework,向其中添加 plugin 基類:AbstractPlugin。如果你忘記怎樣創建和使用 framework,請參看前文:
深入淺出 Cocoa 之 Framework。

AbstractPlugin 類僅僅提供兩個接口:name 用來標識 plugin,run 用來供宿主程序運行插件。
- (NSString *)name;
- (IBAction)run:(id)sender;2,在 PluginDemo 中連接和使用該 framework 來運行插件。如果你忘記怎樣連接和使用 framework,請參看前文:
深入淺出 Cocoa 之 Framework。我們在按鈕響應函數中,運行選中的插件。
- (IBAction)runPlugin:(id)sender
{
AbstractPlugin *plugin = [[pluginsController selectedObjects] lastObject];
if (!plugin)
return;
[plugin run:sender];
}
創建 plugin
1,創建 plugin;

3,創建 UI 界面;

4,創建繼承自基類的 plugin 子類:PluginOne;

PluginOne 類繼承自 AbstractPlugin,它僅僅是顯示和隱藏一個 window,其實現如下:
#import "PluginOne.h"
@implementation PluginOne
@synthesize mainWindow;
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
[NSBundle loadNibNamed:@"PluginOneMainWindow" owner:self];
}
return self;
}
- (void)dealloc
{
mainWindow = nil;
[super dealloc];
}
- (NSString *)name;
{
return @"Plugin One";
}
- (IBAction)run:(id)sender;
{
[mainWindow center];
[mainWindow makeKeyAndOrderFront:sender];
}
- (IBAction)closeWindow:(id)sender;
{
[mainWindow orderOut:sender];
}
@end
5,plugin 設置
下面我們來對 plugin 進行設置,我們可以設置其 Principal class,Wrapper Extension(擴展名)。

使用 plugin
1,宿主程序設置
前面說了,在這個例子中,我們打算將插件隨宿主程序一起發布,所以其存放位置就在宿主應用程序包中。因此我們需要在宿主程序種添加一個 Add Copy Files 的 build phase,如下所示:

2,載入plugin
在正式的應用中,我們應該在前面提到的三個目錄下去查找所有 plugin,因為這三個目錄都是 Cocoa 所推薦的 plugin 目錄。在這個例子中,演示的是隨宿主應用程序一起發布的程序,所以我只掃描了應用程序包中的目錄。
- (NSArray *)loadPlugins
{
NSBundle *main = [NSBundle mainBundle];
NSArray *allPlugins = [main pathsForResourcesOfType:@"bundle" inDirectory:@"../PlugIns"];
NSMutableArray *availablePlugins = [[[NSMutableArray alloc] init] autorelease];
id plugin = nil;
NSBundle *pluginBundle = nil;
for (NSString *path in allPlugins) {
pluginBundle = [NSBundle bundleWithPath:path];
[pluginBundle load];
Class principalClass = [pluginBundle principalClass];
if (![principalClass isSubclassOfClass:[AbstractPlugin class]]) {
continue;
}
plugin = [[principalClass alloc] init];
if ([plugin respondsToSelector:@selector(run:)])
{
[availablePlugins addObject:plugin];
NSLog(@" >> loading plugin %@ from %@", [plugin name], path);
}
[plugin release];
plugin = nil;
pluginBundle = nil;
}
return availablePlugins;
}
該函數在 init 中被調用:
- (id)init
{
self = [super init];
if (self) {
plugins = [[self loadPlugins] retain];
//plugins = [[self loadAllPlugins] retain];
}
return self;
}
下面提供一個函數掃描前面提到的三個目錄,你可以用這個函數提到上面代碼中對 loadPlugins 的調用:
- (NSArray *)loadAllPlugins
{
NSString *appName = @"PluginOne/Plugins";
NSString *appSupport = @"Library/Application Support";
appSupport = [appSupport stringByAppendingPathComponent:appName];
NSString *appPath = [[NSBundle mainBundle] builtInPlugInsPath];
NSString *userPath = [NSHomeDirectory() stringByAppendingPathComponent:appSupport];
NSString *sysPath = [@"/" stringByAppendingPathComponent:appSupport];
NSArray* paths = [NSArray arrayWithObjects:appPath, userPath, sysPath, nil];
NSMutableArray * availablePlugins = [[[NSMutableArray alloc] init] autorelease];
for (NSString * path in paths)
{
NSLog(@" >> Search in directory: %@", path);
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
for (NSString *fileName in contents)
{
if ( [[fileName pathExtension] isEqualToString:@"plugin"] || [[fileName pathExtension] isEqualToString:@"bundle"])
{
NSString *fullPath = [path stringByAppendingPathComponent:fileName];
NSBundle *pluginBundle = [NSBundle bundleWithPath:fullPath];
if (pluginBundle && [pluginBundle load])
{
Class principalClass = [pluginBundle principalClass];
if (![principalClass isSubclassOfClass:[AbstractPlugin class]]) {
continue;
}
id plugin = [[principalClass alloc] init];
if ([plugin respondsToSelector:@selector(run:)])
{
[availablePlugins addObject:plugin];
NSLog(@" >> loading plugin %@ from %@", [plugin name], path);
}
[plugin release];
plugin = nil;
}
}
}
}
return availablePlugins;
}
運行結果顯示 plugin 列表的 popupbutton 的內容被綁定到該 plugins 數組,所以程序啟動之后,就能顯示 plugin 的列表。運行結果如下:

點擊運行之后,就能顯示出插件主界面:
Reference:
Code Loading Programming Topics provides information about writing plug-ins using the Objective-C language.
Bundle Programming Guide provides an overview to bundles, including their purpose, types, structure, and the API for accessing bundle resources.