深入理解QtCreator的插件设计架构

+++
date = "2017-04-28T00:59:02+08:00"
draft = true
title = "深入理解QtCreator的插件设计架构"
blog ="blog.qizr.tech"
+++

基于插件的设计好处很多,把扩展功能从框架中剥离出来,降低了框架的复杂度,让框架更容易实现.扩展功能与框架以一种很松的方式耦合,两者在保持接口不变的情况下,可以独立变化和发布,将软件的复杂度限制在了单个的插件之中,比较适用与需求不定或是业务容易发生变化的软件设计.

1.架构描述

个人感觉,《Software Architecture Patterns》对该架构的描述比较准确,如下图所示.

插件架构

点击这里可以下载这篇文章.
该架构需要关注以下三部分,

1.1核心系统

核心系统包含两部分功能,1.最小功能集合,提供给各个插件模块使用,也就是插件如何使用核心系统的功能进行功能扩展;2.插件模块的生命周期管理.

1.2插件模块

插件模块用于增强或扩展核心系统以产生额外的业务功能,插件模块应该是高度内聚,尽量避免产插件之间的依赖.

1.3契约

这里的契约包含了核心模块和插件模块的通信协议,模块之间不建议发生任何依赖.常见通信方式包含插件会提供一些虚函数,供核心系统中的模块加载器进行初始化,销毁等工作,核心系统提供一些函数,供具体插件模块使用,还可以通过soap等远程通信方式完成两者之间的通信.

2.qtcreator案例

提到插件架构总会想到eclipse,其实众多IDE,编辑器,都采取了插件架构,例如QTCreator,sumblime等,下面结合这些实际的例子,看下这些优秀的软件是如何将插件模式落地的.

qtcreator是一款适用于qt开发的跨平台IDE,这里重点关注这款ide的设计,拿这个作为例子是因为本人在工作期间用的比较多.

qtcreator(4.2)界面:长得多像一个IDE啊

qtcreator

架构描述

QtCreator是由插件加载器和一堆插件构成的.如图所示.


QtCreator架构

其中PluginManage负责插件的加载,管理,销毁等工作.插件中有一个叫core的插件,是QtCreator最基础的插件,提供了向界面增加菜单等功能,具体可以参考如何编写qt插件.

2.1核心系统

可以看出,QtCreator的核心系统是由PluginManager和Core插件构成.前者负责插件的管理工作,后者负责提供QtCreator的最小功能集合,在PluginManager面前Core是当做普通插件进行加载,在自定义插件面前Core就是一个基础功能库,使用该库可以扩展QtCreator的功能.

QtCreator的所有功能,全是由插件实现,这种思路的优点是简化了顶层业务,也就是插件管理工作的逻辑,在那里只有PlunginManager和Plugin,缺点是增加了加载插件的复杂度,因为基础库这个插件需要被其他插件依赖,所以creator在插件加载时就必须要考虑插件之间的依赖性,这个在后面可以看到qtcreator是如何解决这个问题的.

core插件:

core就是插件

自定义插件使用core向界面添加菜单:

自定义插件使用core向界面添加菜单

这里可以做个验证,QtCreator有个"关于插件"的菜单项,可以尝试把所有插件都移除,可以发现其中core插件是灰的,不能进行移除.

关于插件:

关于插件

重启creator后,可以看到,基本就剩一个界面了.

裸体的qtcreator:

没有插件的creator

有读者可能会问,这不还是有个界面嘛,这剩下眼睛能看到界面就是core插件完成的了,可以看到core插件初始化的代码中定义了MainWindow,所以说core插件是creator不能没有的.

core插件初始化:

core插件初始化

2.2插件模块

插件都需要继承IPlugin的接口,关于IPlugin的描述参见官方文档,只说下重点,插件是由描述文件和继承IPlugin的类库组成.

描述文件:

描述文件

描述文件描述了插件的基本信息,用于被插件管理器加载,最后一行描述了该插件所依赖的其他插件,PluginManage会根据插件之间的依赖关系决定加载顺序,详见这里.

IPlugin是插件的基类接口,列举下主要接口,详见这里.

//初始化函数,在插件被加载时会调用
bool IPlugin::initialize(const QStringList &arguments, QString *errorString)

//在所有插件的initialize函数被调用后,调用该函数,此时该插件依赖的插件已经初始化完成
void IPlugin::extensionsInitialized()

//在所有插件的extensionsInitialized函数调用完成以后进行调用
bool IPlugin::delayedInitialize()

关于以上两个函数的调用顺序可以参见官网的说法:

1.All plugin libraries are loaded in root-to-leaf order of the dependency
tree.

2.All plugins' initialize functions are called in root-to-leaf order of the
dependency tree. This is a good place to put objects in the plugin manager's object pool.

3.All plugins' extensionsInitialized functions are called in leaf-to-root order of the dependency tree. At this point, plugins can be sure that all plugins that depend on this plugin have been initialized completely (implying that they have put objects in the object pool, if they want that during the initialization sequence).

2.3契约

契约包含两部分,1.核心系统如何加载插件,2.插件如何使用核心系统为软件扩展功能

核心系统如何加载插件

结合上文中的分析,我们结合源码来看下,QtCreator的插件加载机制,从main函数开始看,能够找到下面这个函数.

PluginManager::loadPlugins();

让我们跟下去,看看loadPlugins()里面的源码,通过源码和注释基本能够看清qtcreator加载插件的机制.

void PluginManagerPrivate::loadPlugins()
{
    //1.获取待加载的插件,loadQueue函数将插件根据彼此的依赖关系进行了排序,
    //有兴趣的同学可以关注下这个函数
    QList<PluginSpec *> queue = loadQueue();
    //2.加载插件
    foreach (PluginSpec *spec, queue) {
        loadPlugin(spec, PluginSpec::Loaded);
    }
    //3.初始化插件,调用每个插件的initialize函数
    foreach (PluginSpec *spec, queue) {
        loadPlugin(spec, PluginSpec::Initialized);
    }
    //4.按照相反顺序调用每个插件的extensionsInitialized函数
    Utils::reverseForeach(queue, [this](PluginSpec *spec) {
        loadPlugin(spec, PluginSpec::Running);
        if (spec->state() == PluginSpec::Running) {
            delayedInitializeQueue.append(spec);
        } else {
            // Plugin initialization failed, so cleanup after it
            spec->d->kill();
        }
    });
    emit q->pluginsChanged();

    delayedInitializeTimer = new QTimer;
    delayedInitializeTimer->setInterval(DELAYED_INITIALIZE_INTERVAL);
    delayedInitializeTimer->setSingleShot(true);
    connect(delayedInitializeTimer, &QTimer::timeout,
            this, &PluginManagerPrivate::nextDelayedInitialize);
    //5.延时调用每个插件的delayedInitialize函数
    delayedInitializeTimer->start();
}


关于解决插件依赖问题可以查看上文源码中loadQueue函数,具体的插件加载机制可以查看源码中loadPlugin函数,QtCreator的源码还是比较容易理解的.

插件如何使用核心系统

通过官网的教程,我们可以创建一个自己的creator插件,并能够了解自己的插件如何使用核心系统提供的功能,但是总感觉不过瘾,所以让我们来分析一个creator自带的插件,这里我选取git插件.

git插件:

git插件

该插件覆盖了git基本操作,我以基本的log命令操作入手,来分析与核心系统的交互,完成Log操作以后,界面显示如下,主编辑区显示了log信息.


log界面

,首先找到git插件代码,git插件对应下面这个类

class GitPlugin : public VcsBase::VcsBasePlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Git.json")

public:
...

找到log对应的函数对应的槽函数logRepository,调用了m_gitClient的log函数,继续跟踪进去.

void GitPlugin::logRepository()
{
    const VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    m_gitClient->log(state.topLevel());
}

里面代码较多,只关注重点的几行

void GitClient::log(const QString &workingDirectory, const QString &fileName,
                    bool enableAnnotationContextMenu, const QStringList &args)
{
   ...
   //1.从核心系统获取Creator的主编辑区
    VcsBaseEditorWidget *editor = createVcsEditor(editorId, title, sourceFile,
                                                  codecFor(CodecLogOutput), "logTitle", msgArg);
   ...
  // 2.开始拼接git的Log命令参数
    QStringList arguments = { "log", noColorOption, decorateOption };
    int logCount = settings().intValue(GitSettings::logCountKey);
    if (logCount > 0)
        arguments << "-n" << QString::number(logCount);

    auto *argWidget = editor->configurationWidget();
    argWidget->setBaseArguments(args);
    QStringList userArgs = argWidget->arguments();

    arguments.append(userArgs);

    if (!fileName.isEmpty())
        arguments << "--follow" << "--" << fileName;
    //3.执行git命令,并将命令结果显示在主编辑区
    vcsExec(workingDir, arguments, editor);
}

VcsCommand *VcsBaseClientImpl::vcsExec(const QString &workingDirectory, const QStringList &arguments,
                                       VcsBaseEditorWidget *editor, bool useOutputToWindow,
                                       unsigned additionalFlags, const QVariant &cookie) const
{
    //4.创建命令
    VcsCommand *command = createCommand(workingDirectory, editor,
                                        useOutputToWindow ? VcsWindowOutputBind : NoOutputBind);
    command->setCookie(cookie);
    command->addFlags(additionalFlags);
    if (editor)
    //5.得到编辑区指针
        command->setCodec(editor->codec());
    //6.进入命令队列执行,并将结果显示在编辑区内
    //典型的命令模式!
    enqueueJob(command, arguments);
    return command;
}

以上可以看出,log命令使用了核心系统的交互是在获取主编辑区那里,剩余就是插件内部的处理逻辑了.

3.扩展

以上了解了creator的设计思路,扩展一下,在消息中间件,微服务盛行的今天,核心系统和插件完全可以设计成不在一个进程当中,我们可以把核心系统和插件之间通过远程调用的方式进行联系,核心系统与插件均可设计为单个进程或者服务,让整个系统的部署更加灵活,单个插件的问题也不会影响到整个系统.当然,核心系统与插件之间的发现使用机制,是需要我们结合实际使用场景进一步深入思考的.

4.总结

本文首先介绍了插件设计架构的理论基础,然后又结合了QtCreator的源码对跟架构如何落地进行了分析,希望能够帮助到想要了解插件架构的同学们.

参考

qtcreator插件开发
Qt Creator Manual

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容