1. 程式人生 > >Qt的內存管理機制

Qt的內存管理機制

amp bject 一點 nec 好的 signal 時代 執行 bool

當我們在使用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機制的影響;
  • 對於QWidgetparent為NULL時代表其為一個頂層窗口,也可以就是獨立於其他widget在系統任務欄單獨出現的widget,對於永遠都是頂層窗口的widget,例如QDialog,當parent不為NULL時他會顯示在父widget中心區域的上層;
  • 如果QWidgetparent為NULL或是其他值,在其加入布局管理器或者QMainWindow設置widget時,會自動將parent設置為相應的父widget,在父控件銷毀時這些子控件以及布局管理器對象會一並銷毀。

所以我們可以看出,QObject對象實際上擁有一顆類實例關系樹,在樹中保存了所有通過指定parent註冊的子對象,而子對象裏又保存有其子對象的關系樹,所以當一個父對象被銷毀時,所有依賴或間接依賴於它的對象都會被正確的釋放,使用者無需手動管理這些資源的釋放操作。

基於此原理,我們可以放心的讓Qt管理資源,這裏有幾個建議:

  1. 對於QObject及其派生類,如果彼此之間存在一定聯系,則應該盡量指定parent,對於QWidget應該指定parent或者加入布局管理器由管理器自動設置parent。
  2. 對象只需要在局部作用域存在時可以選擇不進行內存分配,利用局部作用域變量的生命周期自動清理資源。
  3. 對於非QWidget的對象來說,如果不指定非NULLparent,則需要自己管理對象資源。QWidget比較特殊,我們在下一節講解。
  4. 對於在局部作用域上創建的父對象及其子對象,要註意對象銷毀的順序,因為父對象銷毀時也會銷毀子對象,當子對象會在父對象之後被銷毀時會引發double free。

QWidget和內存的釋放

QWidget也是QObject的子類,所以在parent機制上是沒有區別的,然而實際使用時我們更多的是使用“關閉”(close)而不是delete去刪除控件,所以差異就出現了。

先提一下widget關閉的流程,首先用戶觸發close()槽,然後Qt向widget發送QCloseEvent,默認的QCloseEvent會做如下處理:

  1. 將widget隱藏,也就是hide()
  2. 如果有設置Qt::WA_DeleteOnClose,那麽會接著調用widget的析構函數

我們可以看到,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提供了一套方便的機制幫助我們進行內存和資源管理,使我們從繁重的勞動中得到了部分的解放,但同時也要註意到那些很容易坑,這樣才能寫出健壯的正確執行的程序。

如有錯誤之處,歡迎批評指正。

參考:

http://doc.qt.io/qt-5/qwidget.html

http://doc.qt.io/qt-5/qobject.html

http://doc.qt.io/qt-5/objecttrees.html

https://stackoverflow.com/questions/20164015/is-deletelater-necessary-in-pyqt-pyside

Qt的內存管理機制