首页 Qt Creator 源码学习 Qt Creator 源码学习 09:IPlugin

Qt Creator 源码学习 09:IPlugin

14 2.5K

上一章我们说到,PluginSpec描述了插件的“元数据”。这些“元数据”是静态数据,读取自一个插件描述文件。而所有的 Qt Creator 插件,都要继承IPlugin类。

IPlugin类是所有插件的基类,每个插件都必须继承这个抽象类,并实现其中的纯虚函数(这是一个不恰当的命名,原本类名的前缀I代表 interface,但随着版本的迭代,这个类已经变成一个抽象类而不是接口。像 C++ 这种语言,并不区分 class 与 interface,所以这样带有前缀的名字很容易被人弄混,这是我们自己应该避免的)。Qt Creator 的插件包含两部分:一个描述文件(这个我们之前已经详细介绍过了)和一个至少包含了IPlugin实现的库文件。在这个库文件中,插件实现的IPlugin类需要与描述文件中的name属性相匹配。这个实现需要使用标准的 Qt 插件系统暴露给外界,使用Q_PLUGIN_METADATA宏,并且给定 IID 为org.qt-project.Qt.QtCreatorPlugin

Q_PLUGIN_METADATA宏是 Qt 5.0 新引入的。Qt 5 与 Qt 4 的插件机制完全不同,所以,这里只简单介绍 Qt 5 的插件机制。这个宏用于声明插件的元数据。当实例化插件对象时,这些元数据会作为该对象的一部分。这个宏需要声明一个 IID 属性,用于标识对象实现的接口;还需要一个文件的引用,该文件包含了插件的元数据。任何一个 Qt 插件都需要在源代码中添加这么一个宏,并且只允许添加一个。例如:

class MyInstance : public QObject
{
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDummyPlugin" FILE "mymetadata.json")
};

使用这个宏的类必须有无参数的构造函数。FILE是可选的,指向一个 JSON 文件。这个文件必须能够被构建系统找到。

在这里,Qt Creator 规定其插件的 IID 必须是 org.qt-project.Qt.QtCreatorPlugin。而这个 IID 所代表的,就是这个IPlugin

IPlugin提供了一系列回调函数,用于插件加载到一定阶段时进行回调。下面我们按照插件加载的顺序介绍IPlugin提供的一些函数。

当插件描述文件读取、并且所有依赖都满足之后,插件将开始进行加载。这一步分为三个阶段:

  1. 所有插件的库文件按照依赖树从根到叶子的顺序进行加载。
  2. 按照依赖树从根到叶子的顺序依次调用每个插件的IPlugin::initialize()函数。该函数的签名如下:
virtual bool initialize(const QStringList &arguments, QString *errorString) = 0;

这是一个纯虚函数,因此每个插件都必须实现这个函数。该函数会在插件加载完成,并且创建了插件对应的IPlugin对象之后调用。该函数返回值是bool类型,当插件初始化成功,返回true;否则,需要在errorString参数中设置人可读的错误信息。注意,该函数的调用顺序是依赖树从根到叶子,因此,所有依赖该插件的插件的initialize()函数,都会在该插件自己的initialize()函数之后被调用。如果插件需要共享一些对象,就应该将这些共享对象放在这个函数中。
3. 按照依赖树从叶子到根的顺序依次调用每个插件的IPlugin::extensionsInitialized()函数。该函数的签名如下:

virtual void extensionsInitialized() = 0;

注意,这也是一个纯虚函数。该函数会在IPlugin::initialize()调用完毕、并且自己所依赖插件的IPlugin::initialize()IPlugin::extensionsInitialized()调用完毕之后被调用。在这个函数中,插件可以假设自己所依赖的插件已经成功加载并且正常运行。当运行到这一阶段时,插件所依赖的其它插件都已经初始化完毕。这暗示着,该插件所依赖的各个插件提供的可被共享的对象都已经创建完毕(这是在IPlugin::initialize()函数中完成的),可以正常使用了。

如果插件的库文件加载失败,或者插件初始化失败,所有依赖该插件的插件都会失败。

除了IPlugin::initialize()IPlugin::extensionsInitialized()两个函数之外,另外一个与启动有关的函数是

virtual bool delayedInitialize() { return false; }

该函数会在所有插件IPlugin::extensionsInitialized()函数调用完成、同时所依赖插件的IPlugin::delayedInitialize()函数也调用完成之后才被调用,也就是延迟初始化。插件的IPlugin::delayedInitialize()函数会在程序运行之后才被调用,并且距离程序启动有几个毫秒的间隔。为避免不必要的延迟,插件对该函数的实现应该尽快返回。该函数的意义在于,有些插件可能需要进行一些重要的启动工作;这些工作虽然不必在启动时直接完成,但也应该在程序启动之后的较短时间内完成。该函数默认返回false,即不需要延迟初始化。如果插件有这类需求,就可以重写这个函数。

说完了启动时可能用到的函数,接下来是关闭时会用到的函数:

virtual ShutdownFlag aboutToShutdown() { return SynchronousShutdown; }

aboutToShutdown()函数会以插件初始化的相反顺序调用。该函数应该用于与其它插件断开连接、隐藏所有 UI、优化关闭操作。如果插件需要延迟真正的关闭,例如,需要等待外部进程执行完毕,以便自己完全关闭,则应该返回IPlugin::AsynchronousShutdown。这么做的话会进入主事件循环,等待所有返回了IPlugin::AsynchronousShutdown的插件都发出了asynchronousShutdownFinished()信号之后,再执行相关操作。该函数默认实现是不作任何操作,直接返回IPlugin::SynchronousShutdown,即不等待其它插件关闭。自然,IPlugin有一个相关的信号:

signals:
    void asynchronousShutdownFinished();

前面已经多次提到了一个概念:可被共享的对象。插件之间的依赖容易解决。比如,插件 B 依赖于插件 A,那么,如果 B 成功加载,我们可以肯定,A 一定是存在的。那么,我们就可以在 B 中使用 A 提供的各个函数。这一点很好理解。但是,还有另外一种情况:插件 B 扩展插件 A 的功能。例如,不同的插件提供了打开不同文件的功能:插件 A 提供打开文本文件的功能;插件 B 提供打开二进制文件的功能。那么,当我们的程序启动时,程序怎么知道自己能打开哪些文件呢?因为在程序启动时,并不知道哪些插件是可用的。Qt Creator 提供了一个对象池。A 可以打开文本,B 可以打开二进制文件,那么,把能打开文件的类实例放到对象池里面。程序启动之后,自己去对象池找,看有哪些关于打开文件的对象,就知道自己能打开什么文件了。

对象池可以理解为一个全局变量,里面会有很多不同插件提供的对象,其它插件只需要从这里面找到自己需要的对象即可使用。这种设计避免了插件要暴露很多接口的情况。如果没有对象池,那么,上面所说的插件 A 和 B 就需要主动注册自己能够打开哪些文件,这就要求主程序提供类似的注册接口——进一步说,程序需要将所有可能被扩展的功能都提供注册接口,这无疑会引起接口“爆炸”。而对象池的设计恰恰避免了这一点。对象池很像一个 IoC 容器,有自己的“好莱坞原则”。

有三个函数与对象池有关:

void addObject(QObject *obj)
{
    PluginManager::addObject(obj);
}
void addAutoReleasedObject(QObject *obj)
{
    d->addedObjectsInReverseOrder.prepend(obj);
    PluginManager::addObject(obj);
}
void removeObject(QObject *obj)
{
    PluginManager::removeObject(obj);
}

IPlugin将对象池操作委托给PluginManager类。

值得一说的是addAutoReleasedObject()函数。通常,被添加到对象池的对象必须要手动析构。这里,IPlugin提供了一个工具函数,能够帮助自动释放某些对象,即这里的 auto-released 对象。我们可以在Internal::IPluginPrivate类中找到addedObjectsInReverseOrder的定义:

namespace Internal {
class IPluginPrivate
{
public:
    PluginSpec *pluginSpec;
    QList<QObject *> addedObjectsInReverseOrder;
};
} // namespace Internal

该属性为QList类型,使用prepend()函数,每次在列表头部添加对象。因此,这里实际保存的是被添加顺序的倒序,所以可以用于以相反的顺序析构被添加的对象。有关对象池的实现细节,我们会在后面的章节中详细介绍。

另外,IPlugin还提供了相关联的PluginSpec实例的访问:

PluginSpec *IPlugin::pluginSpec() const
{
    return d->pluginSpec;
}

14 评论

enoorez 2017年12月19日 - 15:10

期待下一章

回复
zfy 2019年5月7日 - 14:29

期待更新!

回复
007 2019年9月9日 - 11:12

好久没更新呢

回复
豆子 2019年9月19日 - 15:31

最近有点忙,没有及时更新。等过去这段忙的时间就会好些了。感谢关注

回复
jinlong 2019年12月16日 - 15:01

期待更新!

回复
lucky 2020年4月28日 - 18:04

期待更新

回复
update 2020年7月22日 - 09:55

啥时候继续学Qt源码啊,期待更新

回复
豆子 2020年7月27日 - 22:18

最近都没有看 Qt,工作上面一直不用,抱歉拖了这么久

回复
吴冬亮 2020年8月4日 - 16:52

哈哈,还是没更新。

回复
walter 2020年9月25日 - 13:42

期待豆子的大作

回复
lucky dog 2020年12月19日 - 23:33

膜拜一下大神、期待更新~

回复
suomier 2021年5月29日 - 00:23

期待大佬的更新

回复
evan 2022年2月7日 - 16:26

您好,我是刚接触QTC框架开发的,看完了您写的文章很有帮助,关于安装程序这部分我想向您请教一下不同的电脑平台怎么自动识别区分,需要怎么去开发实现呢?

回复
豆子 2022年2月7日 - 22:47

具体 Qt 的实现我不大了解,不过如果是安装程序的话,一般是由安装脚本实现的。如果你不是完全自己写安装的话,安装程序的框架一般是有内置的工具的,否则就需要自己去按照不同系统检测

回复

回复 update 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2