Cocoa插件架构

Posted by CoderLeonidas on March 18, 2020

Cocoa插件架构

本章节描述如何通过插件为app构建可扩展性。如果您希望使您的应用程序模块化、可定制、易于扩展,那么您应该阅读本节以了解构建插件架构的不同方法。

关于插件体系

对于希望构建模块化、可定制且易于扩展的应用程序的开发人员来说,插件架构是一个很有吸引力的解决方案。对于许多开发人员来说,一种允许第三方在不访问源代码的情况下向应用程序添加特性的聪明方法已经发展成为一种成熟的应用程序开发方法。

将应用程序构造为设计良好的主框架+一组插件将为应用程序开发人员带来许多好处:

  • 您可以非常快速地实现、合并应用程序特性。
  • 因为插件是具有定义良好接口的独立模块,所以可以快速隔离和解决问题。
  • 您可以在不修改源代码的情况下创建应用程序的自定义版本。
  • 第三方可以开发额外的特性,而不需要麻烦原应用程序开发人员。
  • 插件接口可用于包装用不同语言编写的遗留代码。

终端用户还可以从使用带有插件架构的应用程序中获益:

  • 它们可以为特定的工作流定制特性集。
  • 它们可以禁用不需要的特性,从而简化应用程序的用户界面,减少内存占用,并提高性能。

插件是一个bundle,它通过一些定义良好的架构向应用程序(称为主应用程序)添加功能,以实现可扩展性。这允许第三方开发人员在不访问源代码的情况下向应用程序添加功能。这还允许用户通过在适当的文件夹中安装一个新包来向应用程序添加新功能。Screen saver, preference panes, Interface Builder 调色板, Adobe Photoshop 图形过滤器, iTunes music 可视化器 都是插件的示例。您可以在需要添加特定类型模块的多个实例时使用它们,这些模块提供了定义良好的功能单元,例如图形程序中的新导出筛选器、视频编辑程序中的新转换样式或其他类型的功能。

您可以将主应用程序看作是一种拼图游戏,有无限多的地方可以放置新拼图。插件是附加到拼图上的附加部分,插件架构决定允许拼图的形状。如果插件具有错误的形状,它就不能连接其他部分。图1说明了这个比喻:适合的部分将其功能贡献给应用程序;不合身的衣服就会挂在边线上。

插件架构就像一个无止境的拼图游戏:

插件架构通常采用:插件必须实现的方法、函数列表、插件必须使用的基类的形式,但这还不够。作为一个主应用程序开发人员,您不仅必须显式地记录插件的形式,还必须记录函数,通过详细说明每个方法或函数在应用程序的插件环境中必须表现出何种类型的行为才能正确地工作。

插件的剖析和所在位置

插件通常是可加载的bundle,带有定义应用程序新功能的可执行代码。插件可能包含资源,也可能不包含资源,在某些情况下,只需要代码。在极少数情况下,插件可能会添加资源,但不会添加代码,比如OS X屏幕保护程序的. slidesaver模块就是这样

插件通常安装在几个标准位置之一。您可以从任何地方加载它们,但是OS X API层包含了对查找这些标准路径的支持。

标准路径如下(其中applicationSupportDirectory是包含应用程序支持文件的目录)

  • ~/Library/Application Support/applicationSupportDirectory/PlugIns
  • Library/Application Support/applicationSupportDirectory/PlugIns
  • applicationBundleDirectory/Contents/PlugIns

插件架构设计

如果希望应用程序支持插件,则需要定义插件架构,以便第三方开发人员或组织内部的开发人员能够实现定义良好的接口。插件架构由开发人员决定。有意义的特定架构在很大程度上取决于应用程序。然而,大多数插件架构都共享相同的基本计划。

在面向对象的语言中,应用程序开发人员通过指定自定义类的需求来定义插件架构,即允许拼图块的形状。这些需求通常采用抽象基类或方法列表的形式(如Objective-C协议)。这个自定义类称为主体类,与其他支持代码和资源一起作为插件包的一部分。当加载插件时,主应用程序检查它是否符合要求。如果合适,则应用程序向该类请求一个工厂,提供实例来生成实例。如果插件不符合架构,它的代码会被加载,但是无效的工厂永远不会产生一个实例。

C语言不直接支持面向对象的概念,但是您可以定义一个基于入口点回调函数的插件架构,以便与插件对象通信。应用程序开发人员没有定义插件类的需求,而是定义了一组插件要实现的函数,以及一种为不同类型的消息注册回调函数的机制。应用程序查询插件,以查看它是否实现了必要的入口点函数。如果是,则应用程序将调用这些函数来调用插件的功能。在入口点内,插件可以注册回调函数来响应其他类型的消息对于Carbon应用程序中更复杂的行为,您可以使用Core Foundation CFPlugIn的不透明类型来定义一个同时使用C和c++插件的面向对象的架构。

实现插件架构

您可以用许多不同的方式定义插件的架构,每种方式都适合不同的编程环境和插件类型:

  • 定义插件采用的Objective-C协议
  • 为要继承的插件定义一个抽象基类
  • 定义用于插件实现的C函数和注册回调函数的机制
  • 定义一个带有Core Foundation CFPlugIn不透明类型的插件接口

用与编写应用程序相同的语言定义插件接口是最容易的,但这不是必需的。例如,如果您正在编写一个Objective-C Cocoa应用程序,那么最好使用基于协议或基类的Objective-C插件架构。但是,您也可以为非cocoa插件提供基于c的架构,可以使用CFPlugIn,也可以使用一组简单的预定义回调函数。

如果要编写真正的面向对象的插件架构,则需要确定是在基类中提供插件实现的一部分,还是简单地定义一组要实现的方法或函数。 如果插件共享大量功能,并且在主应用程序代码中提供该功能很不方便,那么使用基类是最好的选择。 OS X屏幕保护程序和首选项窗格架构都定义了抽象基类,这些基类为插件提供了一些基本功能。 其他插件,例如数据处理模块,可能不需要任何基本功能,而可以简单地实现一组标准方法。

下面几节讨论Mac应用程序可用的插件模型,以及关于何时使用它们的更多细节。

协议

Objective-C有一个抽象方法列表的概念,这个抽象列表与类继承层次结构是分离的。这使得开发人员可以定义一组相关的功能,而不需要为这些方法定义类或实现。这样,任何类都可以实现这些方法,不管它在继承层次结构中的位置如何。在Objective-C中,这些列表被称为协议。在c++中,只包含纯虚函数的抽象类通过不同的机制实现了与协议本质上相同的目标。

您可以使用协议来定义插件架构。然后,插件开发人员编写一个采用该协议的类,并使用该类作为插件包的主体类。在运行时,主应用程序可以在使用插件之前检查它是否符合协议。

当满足这三个条件中的至少一个时,您应该为您的插件架构使用协议:

  • 不同的插件不太可能共享很多代码,因此基类只包含一个方法列表。
  • 插件开发人员可能希望从各种基类派生插件主体类。
  • 应用程序支持许多不同类型的插件,插件开发人员可能希望编写一个插件来执行几个不同插件类型的工作。

如果插件需要共享一组核心代码(第一个条件),那么您可能需要定义一个基类,以便插件主体类可以从中继承。如果你需要在插件之间共享代码但同时也要支持不同基类 或者 一个多插件类型的插件,你应该把这段代码在应用程序里,并且为插件提供hook以便访问或创建一个插件可以用作一个成员的单独的类。

Cocoa区分正式协议和非正式协议。您可以根据自己的需要使用任何一种协议来定义插件架构。当一个类采用了一个正式的协议时,它本质上是签署了一个协议来实现协议中的所有方法。如果一个类采用了一个协议,但是没有实现它所有的方法,那么编译器会给出一个警告,如果调用了一个未实现的方法,那么运行时错误就会发生。正式协议使用清单1中使用的Objective-C语法定义,方法原型在@protocol@end之间:

Listing 1 A simple formal protocol

1
2
3
4
5
@protocol MyStringProcessing
 
- (NSString *)processString:(NSString *)aString;
 
@end

另一方面,非正式协议是可选方法的列表。 非正式协议通常在NSObject上作为分类实现,因此任何Cocoa类都可以实现其方法,如清单2所示。如果类从其实现中省略了非正式协议方法,则不会给出编译时警告。 但是,如果一个类希望实现一个特定的方法但没有实现,则将发生运行时错误。

Listing 2 A simple informal protocol

1
2
3
4
5
@interface NSObject(MyStringProcessing)
 
- (NSString *)processString:(NSString *)aString;
 
@end

抽象基类

有些类永远不会被实例化,而是作为其他类的基类来继承。这些抽象类将一些不同的子类将使用的方法和实例变量分组到一个公共定义中。抽象类本身是不完整的,但是可能包含一些有用的代码,这些代码可以减少其子类的实现负担。

主应用程序的插件通常共享一些常用功能,因此许多插件架构都是由抽象类定义的,而该抽象类是插件的主体类所继承的。 基类和相关代码通常打包在framework中。 然后,插件开发人员可以在其应用程序中链接该framework,并包含适当的头文件。

OS X中的屏幕保护程序架构是使用抽象基类的一个很好的例子。 所有的屏幕保护程序基本上都做同样的事情:它们在屏幕上绘制某种动画。 在屏幕上绘制所需的许多代码已经由Application Kit中的NSView类处理,因此屏幕保护程序不必重新实现此行为。 此外,所有屏幕保护程序都需要代码来处理动画定时,淡入和淡出以及其他行为。 因此,所有OS X屏幕保护程序都继承自ScreenSaverView类,该类将屏幕保护程序的特有的功能添加到NSView类。

入口点和回调函数

如果您正在用C语言编写一个Carbon应用程序,定义插件架构的最简单方法是定义一组插件必须实现的功能。这类似于Objective-C协议,但是不涉及任何类,函数列表也没有在任何头文件中明确定义,而只是在插件架构的文档中。对于除最简单的插件外的所有情况,您都应该考虑使用Core Foundation CFPlugIn的不透明类型。

许多插件架构定义一个插件入口点,应用程序使用该入口点向插件发送消息。作为对这些消息的响应,插件可以注册回调函数来处理其操作的各个方面。

例如,iTunes可视化插件通过主入口点接收初始化、清理和空闲消息。在初始化阶段,插件注册一个回调函数来处理与可视化相关的消息。通过回调,当播放或暂停一首歌时,以及当其他状态发生变化时,iTunes通知插件,要求插件执行绘图,并发送其他与可视化相关的消息。

主应用程序需要一种方法来实际查找插件的入口点函数或函数。这是通过定义入口点函数的标准名称,并在运行时查找具有适当签名的函数指针来实现的。可以使用Core Foundation CFBundle不透明类型在函数名和函数指针之间进行转换。

有关使用CFBundle加载插件和查找函数的信息,请参阅 Core Foundation编程主题Bundle Programming Guide.

CoreFoundation的CFPlugin

如果您是Carbon host应用程序开发人员,除了最简单的插件情况外,您应该考虑使用Core Foundation CFPlugIn的不透明类型。CFPlugIn使您不必自己设计、实现和测试新的插件模型,因为CFPlugIn为您处理所有基本的插件功能。

Core Foundation的CFPlugIn与Microsoft的组件对象模型(COM)架构的基础兼容。 在此模型中,主应用程序定义一种或多种类型,每种类型均包含一个或多个接口。 插件针对插件支持的每种类型在所有接口中实现所有功能,以及用于生成类型实例的“工厂功能”。 与Cocoa相比,CFPlugIn具有一个优点,即使用通用唯一标识符(UUID)来唯一标识类型,接口和工厂,从而消除了名称和版本冲突

安全考虑

当涉及到安全性时,任何类型的可扩展性都值得关注。因为插件在您的主应用程序的地址空间中运行它们自己的代码,所以对于它们如何处理您的应用程序的地址空间基本上没有理论上的限制。

但是,您可以做一些事情来防止插件架构的意外误用或滥用:

  • 警告您的用户不要安装来自第三方的插件及其可能造成的副作用。安装什么软件其实是取决于他们的,所以要确保他们知道其中的含义。

  • 限制插件对应用程序代码和数据的直接访问。不要为插件提供指向应用程序控制器对象应用程序数据或其他可能被意外误用或故意滥用的信息的指针。

因为没有简单的方法来区分代码的编写是否良好、意图是否良好,所以您只能警告用户,不能直接向插件提供敏感的应用程序数据。通过实现插件架构,您正在以您意想不到的方式(包括积极的和消极的)为使应用程序运行铺平道路。

参考文章

Code Loading Programming Topics