一、插件概述
1.1 什么是插件
插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。
1.2 插件系统系统的组成
插件系统,可以分为三部分:
-
主系统
通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得相应的指针/引用,它可以像任何其他对象一样使用。
-
插件管理器
用于管理插件的生命周期,并将其暴露给主系统。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。
-
插件
插件本身应符合插件管理器协议,并提供符合主系统期望的对象。
实际上,很少能看到这样一个相对独立的分离,插件管理器通常与主系统紧密耦合,因为插件管理器需要最终提供(定制)某些类型的插件对象的实例。
1.3 插件系统存在的意义
一个大型的软件,架构相当复杂,如果有新的开发工程师进入团队,并参与到软件的功能扩展中,如果没有插件,那整个系统需要应该新功能而重新重新,并且新成员熟悉整个软件系统的成本也非常高,而如果有了插件系统,那将有以下优点:
-
开发人员只需按照插件系统规范就可以添加新功能。
-
插件和核心系统可以独立管理,相互之间并无关联。
-
插件功能可以动态卸载和安装。
1.4 插件系统的基本框架
框架图如下:
二、实现插件
2.1 定义插件基类
插件基类定义插件的协议,所有继承的子类都要按照基类的协议,以下是一个简单的相机插件类:
#include <qlist.h>
#include <QtPlugin>
class CameraPluginBase
{
public:
QString Name; //相机名
virtual ~CameraPluginBase(){}
virtual bool OpenCam()= 0; //打开相机
virtual bool CloseCam() = 0; //关闭
virtual bool SetExposureTime(float time) = 0; //设置曝光时间
virtual bool SetGain(float gain) = 0; //设置增益
};
#define CameraPluginBase_IID "Plugin.Test.Camera"
Q_DECLARE_INTERFACE(CameraPluginBase, CameraPluginBase_IID)
//注册当前类为接口 参数1注册类 参数2插件身份
Q_DECLARE_INTERFACE是为了让Qt元对象系统知道该接口,这样主程序可以动态的扫描实现的接口文件,然后加载插件中的功能,CameraPluginBase_IID是用来标识的字符串,改字符串应该让它具有唯一性。
2.2 定义插件子类
插件子类按照基类的协议去实现插件的相应功能,以下是一个简单的海康相机插件类:
#include <qlist.h>
#include <QtPlugin>
#include "CameraPluginBase.h"
class HikCamera : public QObjec, CameraPluginBase
{
Q_OBJECT
Q_INTERFACES(CameraPluginBase)
Q_PLUGIN_METADATA(IID CameraPluginBase_IID FILE "PluginPar.json")
public:
HikCamera();
~HikCamera();
bool OpenCam();
bool CloseCam();
bool SetExposureTime(float time);
bool SetGain(float gain);
};
Q_INTERFACES宏用于告诉Qt该类实现的接口。Q_PLUGIN_METADATA宏包含插件的IID,并指向一个包含插件元数据的Json文件。该Json文件会被编译到插件中,无需安装。
2.3 实现插件的元数据文件
插件的基本功能已经实现,但是插件的数据信息却没有,有时候主程序在调用插件的时候,需要记录插件的开发者信息和开发时间,验证插件的版本信息和依赖关系,而这些数据信息都可以保存在元数据文件中,也即是上面的"PluginPar.json"文件,简单的文件如下:
{
"author" : "Easy Coder",
"date" : "2021/03/20",
"name" : "海康相机",
"version" : "1.0.0",
"dependencies" : []
}
三、插件管理器
插件管理有几个必须的功能:
-
扫描所有插件,生成依赖关系。
-
动态加载和卸载插件
3.1 插件扫描
void CameraPlugin::Scan(const QString &path)
{
if (!QLibrary::isLibrary(path))
return;
// 获取元数据(名称、版本、依赖)
QPluginLoader *loader = new QPluginLoader(path);
QJsonObject json = loader->metaData().value("MetaData").toObject();
d->Names.insert(path, json.value("name").toVariant());
d->Versions.insert(path, json.value("version").toVariant());
d->Dependencies.insert(path, json.value("dependencies").toArray().toVariantList());
delete loader;
loader = nullptr;
}
3.2 插件装载
void CameraPlugin::LoadAll()
{
// 进入插件目录
QDir path = QDir(qApp->applicationDirPath());
path.cd("cameras");
// 初始化插件中的元数据
foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
Scan(info.absoluteFilePath());
// 加载插件
foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
Load(info.absoluteFilePath());
}
void CameraPlugin::Load(const QString &path)
{
// 判断是否是库
if (!QLibrary::isLibrary(path))
return;
// 检测插件依赖
if (!d->Check(path))
return;
// 加载插件
QPluginLoader *loader = new QPluginLoader(path);
if (loader->load()) {
// 如果继承自 Plugin,则认为是自己的插件(防止外部插件注入)。
CameraPluginBase *plugin = qobject_cast<CameraPluginBase *>(loader->instance());
if (plugin) {
d->Loaders.insert(path, loader);
plugin->Register(d->ToolList);
} else {
delete loader;
loader = Q_NULLPTR;
}
}
}
3.3 插件卸载
void CameraPlugin::UnLoadAll()
{
foreach (const QString &path, d->Loaders.keys())
Unload(path);
for (int i = 0; i < d->ToolList.size(); ++i) {
delete d->ToolList[i];
}
}
void CameraPlugin::Unload(const QString &path)
{
QPluginLoader *loader = d->Loaders.value(path);
// 卸载插件,并从内部数据结构中移除
if (loader->unload()) {
d->Loaders.remove(path);
delete loader;
loader = Q_NULLPTR;
}
}
四、总结
要实现一个简单的插件系统并不复杂,但要实现复杂的插件系统,不仅仅要考虑插件的装载和卸载,还要考虑插件的依赖,插件的版本,插件的自动化测试,单元测试,插件的管理和维护。
本文的测试源码已经放到csdn上,有需要的可以下载
https://download.csdn.net/download/qq_40732350/15996479