1. 程式人生 > >深入理解QtCreator的插件設計架構

深入理解QtCreator的插件設計架構

been enqueue watermark base objects 獲取 The 進程 基本操作

+++
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是由插件加載器和一堆插件構成的.如圖所示.
技術分享圖片
其中PluginManage負責插件的加載,管理,銷毀等工作.插件中有一個叫core的插件,是QtCreator最基礎的插件,提供了向界面增加菜單等功能,具體可以參考如何編寫qt插件.

2.1核心系統
可以看出,QtCreator的核心系統是由PluginManager和Core插件構成.前者負責插件的管理工作,後者負責提供QtCreator的最小功能集合,在PluginManager面前Core是當做普通插件進行加載,在自定義插件面前Core就是一個基礎功能庫,使用該庫可以擴展QtCreator的功能.

QtCreator的所有功能,全是由插件實現,這種思路的優點是簡化了頂層業務,也就是插件管理工作的邏輯,在那裏只有PlunginManager和Plugin,缺點是增加了加載插件的復雜度,因為基礎庫這個插件需要被其他插件依賴,所以creator在插件加載時就必須要考慮插件之間的依賴性,這個在後面可以看到qtcreator是如何解決這個問題的.

core插件:
技術分享圖片
自定義插件使用core向界面添加菜單:
技術分享圖片
這裏可以做個驗證,QtCreator有個"關於插件"的菜單項,可以嘗試把所有插件都移除,可以發現其中core插件是灰的,不能進行移除.

關於插件:
技術分享圖片
重啟creator後,可以看到,基本就剩一個界面了.

裸體的qtcreator:
技術分享圖片
有讀者可能會問,這不還是有個界面嘛,這剩下眼睛能看到界面就是core插件完成的了,可以看到core插件初始化的代碼中定義了MainWindow,所以說core插件是creator不能沒有的.

core插件初始化:
技術分享圖片
2.2插件模塊
插件都需要繼承IPlugin的接口,關於IPlugin的描述參見官方文檔,只說下重點,插件是由描述文件和繼承IPlugin的類庫組成.

描述文件:
技術分享圖片
描述文件描述了插件的基本信息,用於被插件管理器加載,最後一行描述了該插件所依賴的其他插件,PluginManage會根據插件之間的依賴關系決定加載順序,
Plugin是插件的基類接口,列舉下主要接口

//初始化函數,在插件被加載時會調用
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基本操作,我以基本的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的插件設計架構