深入理解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是由外掛載入器和一堆外掛構成的.如圖所示.

其中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的原始碼對跟架構如何落地進行了分析,希望能夠幫助到想要了解外掛架構的同學們.