Qt的記憶體管理機制
當我們在使用Qt時不可避免得需要接觸到記憶體的分配和使用,即使是在使用Python,Golang這種帶有自動垃圾回收器(GC)的語言時我們仍然需要對Qt的記憶體管理機制有所瞭解,以更加清楚的認識Qt物件的生命週期並在適當的時機加以控制或者避免進入陷阱。
這篇文章裡我們將學習QObject & parent物件管理機制,以及QWidget與記憶體管理這兩點Qt的基礎知識。
QObject和記憶體管理
在Qt中,我們可以大致把物件分為兩類,一類是QObject
和它的派生類;另一類則是普通的C++類。
對於第二種物件,它的生命週期與管理和普通的C++類基本沒有區別,而QObject
和它的派生類則有以下的顯著區別:
-
QObject
和其派生類可以使用SIGNAL/SLOT機制 -
它們一般會有一個
parent
父物件的指標,用於記憶體管理(後面重點說明) -
對於
QWidget
和其派生類來說,記憶體管理要稍微複雜一些,因為QWidget
需要和eventloop高度配合才能工作(後面也會重點說明)
signal和slot一般來說並不會對記憶體管理產生影響,但是對close()
槽的處理會對QWidget
產生一些影響,所以我們放在後面講解。
那麼先來看一下QObject和parent機制。
QObject的parent
我們時常能看到QWidget或者其他的控制元件的建構函式中有一項引數parent
,預設值都為NULL,例如:
QLineEdit(const QString &contents, QWidget *parent = nullptr); QWidget(QWidget *parent = nullptr, Qt::WindowFlags f = ...);
這個parent
的作用就在於使當前的物件例項加入parent指定的QObject及其派生類的children中,當一個QObject被delete或者呼叫了它的解構函式時,所有加入的children也會全部被析構。
如果parent
設定為NULL,會有如下的情況:
-
如果是構造時直接指定了NULL,那麼當前例項不會有父物件存在,Qt也不能自動析構該例項除非例項超出作用域導致解構函式被呼叫,或者使用者在恰當的實際使用
delete
操作符或者使用deleteLater
方法; -
如果已經指定了非NULL的
parent
,這時將它設定成了NULL,那麼當前例項會從父物件的children中刪除,不再受到QObject & parent機制的影響; -
對於
QWidget
,parent
為NULL時代表其為一個頂層視窗,也可以就是獨立於其他widget在系統工作列單獨出現的widget,對於永遠都是頂層視窗的widget,例如QDialog
,當parent
不為NULL時他會顯示在父widget中心區域的上層; -
如果
QWidget
的parent
為NULL或是其他值,在其加入佈局管理器或者QMainWindow
設定widget時,會自動將parent
設定為相應的父widget,在父控制元件銷燬時這些子控制元件以及佈局管理器物件會一併銷燬。
所以我們可以看出,QObject物件實際上擁有一顆類例項關係樹,在樹中儲存了所有通過指定parent
註冊的子物件,而子物件裡又儲存有其子物件的關係樹,所以當一個父物件被銷燬時,所有依賴或間接依賴於它的物件都會被正確的釋放,使用者無需手動管理這些資源的釋放操作。
基於此原理,我們可以放心的讓Qt管理資源,這裡有幾個建議:
-
對於QObject及其派生類,如果彼此之間存在一定聯絡,則應該儘量指定parent,對於
QWidget
應該指定parent或者加入佈局管理器由管理器自動設定parent。 - 物件只需要在區域性作用域存在時可以選擇不進行記憶體分配,利用區域性作用域變數的生命週期自動清理資源。
-
對於非
QWidget
的物件來說,如果不指定非NULLparent
,則需要自己管理物件資源。QWidget
比較特殊,我們在下一節講解。 - 對於在區域性作用域上建立的父物件及其子物件,要注意物件銷燬的順序,因為父物件銷燬時也會銷燬子物件,當子物件會在父物件之後被銷燬時會引發double free。
QWidget和記憶體的釋放
QWidget
也是QObject
的子類,所以在parent機制上是沒有區別的,然而實際使用時我們更多的是使用“關閉”(close)而不是delete去刪除控制元件,所以差異就出現了。
先提一下widget關閉的流程,首先使用者觸發close()
槽,然後Qt向widget傳送QCloseEvent
,預設的QCloseEvent
會做如下處理:
hide() Qt::WA_DeleteOnClose
我們可以看到,widget的關閉實際是將其隱藏,而沒有釋放記憶體,雖然我們有時會重寫closeEvent
但也不會手動釋放widget。
看一個因為close機制導致的記憶體洩漏的例子,我們在button被單擊後彈出某個自定義對話方塊:
button.ConnectClicked(func (_ bool) { dialog := NewMyDialog() dialog.Exec() })
因為dialog在close時會被隱藏,而且沒有設定DeleteOnClose
,所以Qt不會去釋放dialog,而使用者也無法回收dialog的資源,也行你會說golang的GC不是能處理這種情況嗎,然而遺憾的是GC並不能處理cgo分配的資源,所以如果你期望GC做善後的話恐怕要失望了,每次點選按鈕後記憶體用量都會增加一點,沒錯,記憶體洩露了。
那麼給dialog設定一個parent,像這樣,會如何呢?
dialog.SetParent(self)
遺憾的是,並沒有什麼區別,因為這樣只是把dialog加入父控制元件的children,並沒有刪除dialog,只有父物件被銷燬時記憶體才會真正釋放。
解決辦法也有三個。
第一種是使用deleteLater
,例如:
dialog.DeleteLater()
這會通知Qt的eventloop在下次進入主迴圈的時候析構dialog,這樣一來確實解決了記憶體洩露,不過缺點是會有不可預測的延遲存在,有時候延遲是難以接受的。
第二種是手動刪除widget,適用於parent為NULL的場合:
C++:
delete dialog;
golang:
dialog.DestroyMyDialog()
說明一下,DestroyType
也是qtmoc生產的幫助函式,因為golang沒有解構函式的概念,所以goqt使用生成的該幫助函式顯示呼叫底層C++物件的解構函式。
第三種比較簡單,對於單純顯示而不需要和父控制元件做互動的widget,直接設定DeleteOnClose
即可,close時widget會被自動析構。
當然對於PyQt5來說並不會存在如上的問題,sip庫能很好的與python的GC一起工作。唯一需要注意的是有時底層C++物件已經被釋放,但是上層python物件依然存在,這時使用該物件將導致拋錯。
總結
Qt提供了一套方便的機制幫助我們進行記憶體和資源管理,使我們從繁重的勞動中得到了部分的解放,但同時也要注意到那些很容易坑,這樣才能寫出健壯的正確執行的程式。
如有錯誤之處,歡迎批評指正。
參考:
ofollow,noindex" target="_blank">http://doc.qt.io/qt-5/qwidget.html
http://doc.qt.io/qt-5/qobject.html
http://doc.qt.io/qt-5/objecttrees.html
sary-in-pyqt-pyside" rel="nofollow,noindex" target="_blank">https://stackoverflow.com/questions/20164015/is-deletelater-necessary-in-pyqt-pyside