QT插件学习系列(二) 插件管理器

1,033 阅读4分钟

1、 概述

上文中展示了一个简单的 QT Low-API 插件实例,但是这却满足不了大型应用程序的实际场景,没有扩展性。而插件间的通信、加载卸载(释放内存)、插件元数据、插件生命周期、插件依赖等问题,便是我们要做的。在QT内部,高级 API 有 PluginManager 负责做这些事,但是低级 API 就需要自己写插件管理器来帮助我们解决这些问题。

想象一台 windows 系统的电脑,包含了主机、显示屏、键鼠等部件。假如我们拔掉键盘,电脑不会出错,只是缺失了键盘的功能,因此键盘就可以看做是一个插件。与此同时一台完整的电脑不仅包含了键鼠,还有耳机、音响、光驱、显卡等部件,这些部件其实都可以看成插件。对 windows 来说,这些“插件”都有一个管理者,即为设备管理器。设备管理器负责添加和删除电脑所有的硬件和驱动,因此可以将设备管理器理解为插件管理器。最后一点电脑系统都有自己的内核,一个 windows 系统从启动到关机都是内核在响应,而内核就可以看做加载插件的主程序,仿佛:“一旦你插上,我就能用你来打游戏”。

通过上面的例子可以看到,一个插件化系统从大的方面来说包含三方面:

插件系统

上图中红色圈的地方可引申出一层,即对应 windows 的驱动层,负责和内核通信。而对应到插件系统即为 “Adapter”,负责插件通信,可避免插件间的相互依赖耦合。这个下篇再讲。

2、 实例

首先,下面的代码及类名都是模仿 QT 源码进行编写(当然也存在些许不完善):

#qtpluginmanager.h

#ifndef QTPLUGINSMANAGER_H
#define QTPLUGINSMANAGER_H

#include <QObject>
#include <QPluginLoader>
#include <QVariant>
#include "qtpluginsmanagerprivate.h"
#include <QDir>

class  QtPluginsManager : public QObject
{
    Q_OBJECT

public:
    static QtPluginsManager &getInstance()
    {
        static QtPluginsManager m_instance;
        return m_instance;
    }
public:
    // 获取插件目录
    QDir getPluginPath();
    //加载所有插件
    void loadAllPlugins();
    //扫描JSON文件中的插件元数据
    void scan(const QString &filepath);
    //加载其中某个插件
    void loadPlugin(const QString &filepath);
    //卸载所有插件
    void unloadAllPlugins();
    //卸载某个插件
    void unloadPlugin(const QString &filepath);
    //获取所有插件
    QList<QPluginLoader *> allPlugins();
    //获取某个插件
    QPluginLoader* getPlugin(const QString &name);
private:
    QtPluginsManager()
    {
     d = new QtPluginsManagerPrivate;
    };
    QtPluginsManagerPrivate *d;
};

#endif
# qtpluginsmanagerprivate.h

#ifndef QTPLUGINSMANAGERPRIVATE_H
#define QTPLUGINSMANAGERPRIVATE_H
#include <QString>
#include <QVariant>
#include <QDebug>
#include <QPluginLoader>
class QtPluginsManagerPrivate
{
public:
    //插件依赖检测
    bool check(const QString &filepath);
    QHash<QString, QVariant> m_names; //插件路径--插件名称
    QHash<QString, QVariant> m_versions; //插件路径--插件版本
    QHash<QString, QVariantList>m_dependencies; //插件路径--插件额外依赖的其他插件
    QHash<QString, QPluginLoader *>m_loaders; //插件路径--QPluginLoader实例
};

#endif

类 QtPluginsManagerPrivate 包含了所有插件的元数据集合,即 metadata,这些元数据来自于我们提前定义的 json 文件,且后缀必须为“.json”。对 QT 内部来说,此 json 文件由宏 Q_PLUGIN_METADATA 声明元数据为插件实例化的一部分。但更重要的是 m_dependencies 集合,包含了插件的依赖关系,以此决定插件的加载顺序和是否有必要加载。

#qtpluginmanager.cpp

void QtPluginsManager::loadAllPlugins()
{
    QDir path = QDir(qApp->applicationDirPath());
    path.cd("../plugins");

    foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
    {
        // 扫描所有插件,录入元数据信息
        scan(info.absoluteFilePath());
    }

    foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
    {
        // 加载插件
        loadPlugin(info.absoluteFilePath());
    }
}
#qtpluginmanager.cpp

// 扫描 JSON 文件中的插件元数据
void QtPluginsManager::scan(const QString& path)
{
    /**
     *  判断是否是库,排除pdb等垃圾文件
     * Windows:.dll、.DLL
     * Unix/Linux:.so
     **/

    if (!QLibrary::isLibrary(path))
        return;

    // 获取元数据(名称、版本、依赖)
    QPluginLoader *loader = new QPluginLoader(path);
    QJsonObject json = loader->metaData().value("MetaData").toObject();

    d->m_names.insert(path, json.value("name").toVariant());
    d->m_versions.insert(path, json.value("version").toVariant());
    d->m_dependencies.insert(path, json.value("dependencies").toArray().toVariantList());

    delete loader;
    loader = Q_NULLPTR;
}
#qtpluginmanager.cpp

// 加载插件
void QtPluginsManager::loadPlugin(const QString& path)
{
    // 判断是否是库
    if (!QLibrary::isLibrary(path))
        return;

    // 检测插件依赖(递归调用)
    if (!d->check(path))
        return;

    // 加载插件
    QPluginLoader loader = new QPluginLoader(path);
    if (loader->load()) {
        // 如果继承自 Plugin,则认为是自己的插件(防止外部插件注入)。
        InterfacePlugin *plugin = qobject_cast<InterfacePlugin *>(loader->instance());
        if (plugin)
        {
            d->m_loaders.insert(path, loader);
            plugin->output("plugin");
        }
        else
        {
            delete loader;
            loader = Q_NULLPTR;
        }
    }
}
# qtpluginsmanagerprivate.cpp

#include "qtpluginsmanagerprivate.h"

// 检测插件依赖
bool QtPluginsManagerPrivate::check(const QString& path)
{
    bool status = true;

    foreach (QVariant item, m_dependencies.value(path)) {
        QVariantMap map = item.toMap();
        // 依赖的插件名称、版本、路径
        QVariant name = map.value("name");
        QVariant version = map.value("version");
        QString path = m_names.key(name);

        /********** 检测插件是否依赖于其他插件 **********/
        // 先检测插件名称
        if (!m_names.values().contains(name)) {
            qDebug() << Q_FUNC_INFO << "  Missing dependency:" << name.toString() << "for plugin" << path;
            status = false;
            continue;
        }

        // 再检测插件版本
        if (m_versions.value(path) != version) {
            qDebug() << Q_FUNC_INFO << "    Version mismatch:" << name.toString() << "version"
                     << m_versions.value(m_names.key(name)).toString() << "but" << version.toString() << "required for plugin" << path;
            status = false;
            continue;
        }

        // 然后,检测被依赖的插件是否还依赖于另外的插件
        if (!check(path)) {
            qDebug() << Q_FUNC_INFO << "Corrupted dependency:" << name.toString() << "for plugin" << path;
            status = false;
            continue;
        }
    }

    return status;
}

QT 内部对于插件的版本管理比较严格,作为 Demo 暂不深入讨论。 详见:Qt Documentation Snapshots :Plugin Meta Data。

check 方法检查了是否缺失了某些依赖插件,检查了插件版本是否匹配,假如 A 依赖了B,B 依赖了 C,倘若 C 插件缺失了,则 A 插件也就无法正确加载(因为有可能在 A 的构造函数中用到了 B,B 的构造函数中用到了 C),因此可见,插件还是尽可能不要产生依赖。

image

参考 :
一去丶二三里:构建自己的 Qt 插件系统
深入理解QtCreator的插件设计架构

Demo 地址:
download.csdn.net/download/u0…
github.com/qht10030778…