Qt Creator 源碼學習筆記04,多插件實現原理分析

“閱讀本文大概需要 10?分鐘
插件聽上去很高大上,實際上就是一個個動態(tài)庫,動態(tài)庫在不同平臺下后綴名不一樣,比如在?
Windows
下以.dll
結尾,Linux
?下以.so
結尾開發(fā)插件其實就是開發(fā)一個動態(tài)庫,該動態(tài)庫能夠很好的加載進主程序、訪問主程序資源、和主程序之間進行通信
本篇文章一起學習下?
Qt Creator
當中是如何實現插件的開發(fā)、加載、解析、管理以及互相通信的,方便我們開發(fā)自定義插件打下基礎簡介
Qt Creator
插件理解起來其實很簡單,定義一個接口類作為基類,其他插件需要繼承該類實現對應的虛方法,每個插件作為獨立子工程編譯后生成對應的動態(tài)庫

主函數加載每個插件對象,然后轉化為對應插件實例
然后每個插件各自根據對應業(yè)務邏輯調用接口就行了
當然了,Qt Creator
?在實現過程當中肯定不止這么簡單,插件的加載、解析、卸載等管理還是比較復雜的,非常值得我們去學習
插件組成
整個插件系統(tǒng)由插件管理器、核心插件、其它插件組成,其中核心插件是系統(tǒng)中不可缺少的,其它插件都要依賴核心插件來進行開發(fā)通信
我們先打開?Qt Creator
?插件菜單看看都包含哪些插件

可以看到所有的插件根據類型進行了分組,同一個類型插件同屬一個樹節(jié)點,每個插件后面有個復選框可以控制加載/卸載該插件
每個插件還包含了版本信息以及歸屬作者信息,這些信息都可以通過對象元數據來配置,插件的版本也很有用,我們編寫的插件可以限定在某個版本之間兼容,這個時候版本號就起作用了,詳細實現后面會講解到
我們可以加載、卸載某個插件,但是無論怎么選擇,核心Core
插件是不能卸載的,why? 因為整個插件系統(tǒng)是建立在?Core
?核心插件基礎之上的,離開核心插件其它插件無法存活
所以我們學習的重點應該放在核心插件上,學會后其它插件很容易上手了
插件管理
插件的核心其實就是對插件的管理,這個是本篇的重點,是我們閱讀源碼時需要重點關注的部分,為什么這么說呢,我舉個栗子大家就清楚了
我們日常寫代碼的時候,比如定義一個變量,需要關注的有這么幾點:
變量的名
變量的值
變量的類型
變量的作用域
變量的生命周期
對每個定義的變量都非常清楚它的一些要素,那么肯定不會出錯的
插件也一樣,每個插件到實際開發(fā)當中也是一個個對象,我們定義的對象是什么類型?名字叫什么?它的值是多少?它的作用域范圍是什么?生命周期呢?什么時候創(chuàng)建和釋放?
搞清楚上述這些,對于理解插件管理工作就更進一步了,下面重點來看看插件的生命周期
插件管理器
插件管理器實現主要在PluginManager
?類當中實現,該類管理了所有的插件加載、卸載以及釋放
對象管理池
這個類是一個單例類,主要管理插件對象,可以理解為對象池,詳細實現都封裝在了?d
指針類里面,
我們繼續(xù)進去看看
pluginmanager_p.h
可以看到底層存儲每個對象用的容器是?QList
,從Qt Creator 4.10
版本開始換成了?QVector
來存儲,說起來這兩個容器的區(qū)別讓我想到了,現在最新版本的?Qt
當中,已經把兩者合二為一了
所以使用哪個無所謂了,不過我們還是要搞清楚這兩個容器的區(qū)別,什么時候用Vector
,什么時候用?List
添加對象
這塊核心代碼其實很好理解,每次添加對象前先加鎖,由于使用的是讀寫鎖,不用擔心函數返回死鎖問題,判斷對象是否合法以及是否已經存在,不存在則追加到?list
?當中,最后拋出一個信號,這個信號在外部需要使用的地方可以綁定,比如模式切換里面就使用到了
添加就對應的刪除,原理和添加一樣
- 刪除對象
同樣的把對象從list
?當中進行了刪除,在刪除之前也向外拋出了信號,用法和添加信號配對使用
這里有個疑問,為啥鎖不在函數最開頭加呢?
插件管理
每個插件對象對應到底層是由?PluginSpec
?來實例化的,每個插件使用?list
容器存儲,如下所示
插件核心類實現
閱讀代碼就可以發(fā)現,這個類主要是記錄了每個插件的一些基本信息,那么這些信息是如何賦值的呢?通過插件描述文件來進行自動加載的,后面學習核心插件會看到
有個核心部分代碼,插件依賴項dependencies
,這個主要解決插件之間依賴關系使用,這個類也很簡單很好理解
比如插件A
依賴插件B
和C
,那么在插件A
加載的時候對應的list
當中就包含了B,C
插件信息,必須等到這兩個插件加載完成后才能加載插件A
,這一點很重要
插件加載流程
前面學習了插件管理器當中的一些基本數據結構,現在來看看這些插件是怎么加載進去的,加載順序和流程是怎么樣的
插件加載流程比較復雜一些,同時也是最重要的部分,主要分為下面幾個步驟

下面我們來詳細看看每個步驟都干了哪些工作,源碼面前了無秘密
設置插件 IID
這個id 是全局唯一,加載插件時會首先判斷插件 ID 合法性,用于確定是你自己編寫的插件,這樣可以防止其它插件惡意注冊加載
大家可以想想一下,如果別人也寫了一個類似的插件,那么如果沒有 ID 區(qū)分是不是就能加載進插件系統(tǒng)當中,從而破壞軟件結構?
Qt Creator
?默認的 ID 為?org.qt-project.Qt.QtCreatorPlugin
,每個插件加載時通過宏進行設置
這個宏是為了配合moc
處理器生成插件導出函數,最終在調用插件接口返回實例時能夠準確返回自己。我們寫個?demo
?來驗證下
新建一個插件叫做?PluginDemo
qmake 編譯一下看下中間結果內容:
設置全局配置類
“全局配置,一般存放的是默認值,用于恢復設置使用
設置局部配置類
“存放程序當前配置參數類。比如我們設置某個參數配置保存后會存在某個配置文件中,程序加載時會從該文件加載到
QSettings
對象當中供我們調用
設置插件路徑
“插件路徑一般是我們 exe 程序相鄰路徑下的,比如plugins/xxx.dll,當然也可以為任意路徑下的動態(tài)庫,只要路徑正確合法都可以加載的,可以設置多條插件路徑
比如正常?Qt Creator
啟動時會給兩個路徑分別為:
關于路徑的獲取可以看后面主程序加載部分可以看到
讀取插件信息
“用于讀取插件原對象信息,主要包含三個過程
讀元數據:這里會挨個讀取每個插件,初始化 QPluginLoader,設置名字,為后面加載做準備,可以叫預加載,創(chuàng)建插件實例對象 PluginSpec,存儲到 List 結構當中
檢測依賴關系::用于重新加載分析每個插件依賴關系,是一個雙重循環(huán),每個插件會和其它插件比較一次,最后按照插件名字進行排序
插件改變:向外拋出信號,插件管理窗口用來刷新 view 列表信息
加載插件
“到了這里才開始真正加載插件了,主要包括下面幾個流程
依賴初始化
加載插件:這里里面才會真正去加載初始化每個插件,計算獲取插件加載隊列
加載(PluginSpec::Loaded):
調用 QPluginLoader.load(),真正加載插件,加載成功才可以獲取每個插件方法,存儲插件實例:
初始化(PluginSpec::Initialized)
這里會調用每個插件的初始化函數:initialize(),該函數是純虛函數,每個插件必須重新實現
運行(PluginSpec::Running)
調用每個插件擴展初始化函數:extensionsInitialized(),此時會挨個判斷買個插件狀態(tài)是否在運行,是的話加入到延遲隊列
延遲初始化
“從延遲隊列當中取出那個插件,調用各自延遲初始化函數:delayedInitialize()
插件加載結束
到此整個插件加載結束了,可以看出來,整個插件的加載過程說白了就是動態(tài)庫加載解析然后調用每個動態(tài)庫里面的虛函數來實現的,所有的插件都繼承自共同的基類(接口),原理很簡單,但是要管理這些插件尤其是多種依賴關系并存情況下是非常不容易的
看到這里大家是不是很好奇,為啥不引用頭文件直接可以調用動態(tài)庫里面的方法?這個主要使用?QPluginLoader
?來實現動態(tài)加載動態(tài)庫,這個類很好理解,詳細使用可以看我之前寫的SDK
調用相關文章
包含了使用示例以及對應解析
如何使用 QLibrary 加載動態(tài)庫
核心插件
學習了解清楚了插件如何管理,如何加載,下面來看看核心插件如何實現,以及如何實現自己的插件
插件描述文件
插件描述文件一般用于記錄每個插件的基本信息,必須有,而且字段和用法都是固定的。名字一般取插件名字,結尾一般都是.json.in
“看到這里是不是好奇,我記得自己第一次看到時也很好奇,為啥是
.in
結尾,這個其實是一個模板文件,經過qmake
構建后最終在臨時目錄下會生成最終的去掉.in
的文件
Core.json.in
插件代碼中包含該文件
文件內容大概如下所示:
其實就是一個標準的json
配置文件,每個字段都很清楚,可能有些變量值不清楚,這里一起學習下。比如版本號字段:
很明顯后面是一個變量值,也可以說是宏定義,我們一般定義json
配置都是固定值,這里采用動態(tài)配置方法,其中QTCREATOR_VERSION
?變量是在pro
工程中定義的
這樣做有什么好處呢?想一想
是不是我們下次變更版本號的時候,直接在pro
文件中更改一次,其它引用到該變量的地方都自動同步了,是不是很方便而且可以減少很多出錯(這就是軟件開發(fā)當中的封裝思想)
其實,除過在配置文件中可以引用變量以外,在代碼中也可以直接引用,關于如何使用,可以看我之前寫的一篇文章,詳細介紹了原理以及實現方法
qmake奇淫技巧之字符串宏定義
核心插件初始化
核心插件主要初始化基本界面結構,包含一個QMainWindow
、菜單欄、狀態(tài)欄、模式工具欄、多窗口面板等等
正如第一篇筆記當中寫到,如果只編譯核心插件,那么運行后整個界面長這個樣子

可以看到僅僅包含基本菜單,插件查看,狀態(tài)欄等內容
每個插件都需要實現自己的初始化函數
初始化函數當中首先要注冊所有插件的mime type
類型,這個是從插件元數據當中讀取的,會跳過已經關閉的插件
接著初始化系統(tǒng)主題,主題其實和我們經常用的?qss
?樣式表類似,大概長這個樣子
其實就是一個.ini
文件格式的內容,定義了很多界面樣式相關變量字段,這些字段會一一映射到對應主題管理類當中,這樣相關界面設置樣式就可以直接調用了
接著也是一個很重要的模塊,初始化菜單管理類,這個類管理了菜單欄所有的菜單/Action,以供其它插件模塊訪問
ActionManager
?這個類是一個特殊的單例類,單例對象初始化只能在核心插件當中,雖然提供了單例返回接口,但是首次如果沒有初始化對象返回的是空指針
所有才有了后面兩個友元類的聲明了,這樣可以直接訪問并且初始化對象實例了,核心插件初始化完成后,其它地方可以直接調用單例函數了
接著就是主界面初始化,初始化?mainWindow
實例
主界面實例初始化后,接著會調用主界面的初始化函數,主界面真正初始化了多插件界面實現,如果想要學習多插件界面是如何實現的,可以重點關注下這個初始化函數
最后是編輯模式、查找相關功能初始化,這些功能不是本次重點,后面有需要再詳細看實現思想
主界面初始化
主界面和我們平時創(chuàng)建項目使用的QMainWindow
沒有兩樣,最大的區(qū)別就是Qt Creator
把界面上所有的操作都進行了封裝管理,這樣其它插件也可以進行訪問,更好的對界面系統(tǒng)進行了擴展
主界面我們重點來學習了菜單欄的使用,看看是如何封裝管理的
主要涉及到下面幾個類
ActionContainer
ActionContainerPrivate
MenuActionContainer
MenuBarActionContainer
ActionManager
這些類的關系如下所示

其中?ActionContainer
對象是基類,向外部所有插件暴露,后面訪問某個菜單大部分場景是返回該類指針的
MenuActionContainer
?是菜單欄當中的菜單對象,可以包含?n
?個菜單
MenuBarActionContainer
?是我們的菜單欄,整個?MainWindows
僅此一份實例
最后就是?ActionManager
類了,我們所有的操作均是通過該類來進行,很顯然它是一個單例類,而且整個系統(tǒng)都是可以訪問的
創(chuàng)建菜單欄
創(chuàng)建文件菜單
可以看到使用起來是非常方便的,而且這種通過傳入字符串創(chuàng)建菜單的方式也簡單理解,外部使用的人員完全不用了解函數內部是怎么實現的,只需要根據自己需要傳入規(guī)定格式的字符串即可
每個菜單都有唯一的字符串?ID
來進行區(qū)分,字符串命名嚴格按照菜單格式,比如
這樣的格式也很好理解,Menu
相當于是大菜單,后面一級是每個子菜單,如果該菜單還有子菜單,那么繼續(xù)擴展下去
其它界面菜單欄菜單創(chuàng)建和上面的過程是類似的,可以照貓畫虎寫出來
創(chuàng)建每個 Action
上面創(chuàng)建了界面的菜單欄,但是每個菜單下面還是空的,需要創(chuàng)建對應的?Action
?才行,下面來看看是怎么創(chuàng)建的
第一行代碼通過菜單管理器返回上面創(chuàng)建的「文件」菜單指針,第二行添加了一個分隔符,后面創(chuàng)建了一個Command
對象,這個類是對每個QAction
進行了封裝,同時支持設置快捷鍵等操作,這樣我們后續(xù)的操作就相當于是一個command
這樣我們的菜單欄就創(chuàng)建初始化完成了,剩下的就是左側模式工具條以及中央內容區(qū)域的創(chuàng)建了
限于篇幅原因,這些內容我們后面再看
App 程序初始化
前面花費了大量篇幅來介紹插件的管理以及主界面的實現,下面我們來看看主程序是如何初始化的
主函數?main.cpp
?里面的內容挺多的,我們看主要加載流程就行了
設置系統(tǒng)配置對象指針
主要是系統(tǒng)當中的一些配置,插件管理器需要記錄那些插件被禁用了,這樣在后面插件初始化時可以跳過了
其中很重要的設置插件ID
,這個作為插件唯一標識符,用來區(qū)分惡意插件,如果別人不知道你的軟件插件IID
,那么他編寫的插件放入你程序目錄下是可以直接通過這個IID
過濾掉的
設置插件路徑
這里的插件路徑包含了兩部分,一部分是我們程序目錄下的插件目錄,另一個是公共目錄,比如下面這個
這一步走完后,如果沒有錯誤整個插件都加載完成了
異常判斷
想法是美好的,但是事實總不如愿,插件在加載過程中可能會遇到一些問題導致加載異常,這樣程序就無法正常運行了,需要拋出錯誤給用戶
這段代碼對核插件加載狀況進行了判斷,如果有錯誤沒有加載完成或者被禁用了,那么就直接返回了。理論上來說核心插件是無法被禁用的,但是如果有人惡意修改配置文件禁用了核心插件,那么此時程序會無法正常啟動的
加載插件
這一步其實是最重要的,上面設置插件路徑后僅僅是讀取每個插件對象,此時這些對應都是靜態(tài)的,到了這一步才真正動起來
關于插件加載這個流程最前面插件管理器當中介紹清楚了,這里我們直接略過就行了
好了關于插件加載學習就到這里了
總結
插件部分內容還是挺長,初次學習源碼的朋友可能會感覺到無從下手、一臉茫然,不用擔心,我第一次也是這種感覺,遇到不懂不理解的小標記下,先理解掌握整體設計思想和流程,再慢慢逐個模塊攻破
軟件開發(fā)也是這個道理,一開始你不可能考慮到所有模塊細節(jié),把握整體結構沒有問題,再挨個實現細節(jié)部分
Qt Creator
非常值得我們多看、多寫的,所謂好事多磨么,看的多了也就明白了一些道理
我們日常開發(fā)過程中遇到的一些問題,可能Qt Creator
當中早就實現好了,可以直接拿來使用,比如像奔潰dump
管理、日志管理、網絡通信、特殊控件實現等都可以拿來直接用的
希望本次分享的筆記對你有幫助,如果覺得有用不妨關注下,有任何問題可以互相交流學習
PS:文中涉及到相關流程圖以及對應源碼,如果感興趣可以后臺私信發(fā)給你
如果覺得對你有幫助,歡迎留言互相交流學習
推薦閱讀
Qt Creator 源碼學習筆記01,初識QTC
Qt Creator 源碼學習筆記02,認識框架結構結構
Qt Creator 源碼學習筆記03,大型項目如何管理工程