翻譯|行業資訊|編輯:鮑佳佳|2020-12-04 10:12:10.090|閱讀 1159 次
概述:Qt 6具有很多新功能。我們添加的最令人興奮的功能之一是將QML和Qt Quick綁定的概念帶回到Qt的核心,并允許從C ++使用它。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關鏈接:
Qt是一個跨平臺框架,通常用作圖形工具包,它不僅創建CLI應用程序中非常有用。而且它也可以在三種主要的臺式機操作系統以及移動操作系統(如Symbian,Nokia Belle,Meego Harmattan,MeeGo或BB10)以及嵌入式設備,Android(Necessitas)和iOS的端口上運行。現在我們為你提供了免費的試用版。趕快點擊下載Qt6最新試用版>>
工具推薦:
Qt 6具有很多新功能。我們添加的最令人興奮的功能之一是將QML和Qt Quick綁定的概念帶回到Qt的核心,并允許從C ++使用它。
Qt 5中的綁定
讓我們首先回顧一下Qt 5中屬性綁定的工作方式。在那里,綁定支持僅限于Qt Quick。這是一個非常簡單的示例:
import QtQuick 2.15 Rectangle { height: width border.width: height/10 }
這樣做的目的是在一個 Rectangle 對象上設置兩個綁定。第一個綁定確保Rectangle永遠是正方形。第二個綁定將邊框寬度設置為高度的10%。然后,Qt中的QML引擎確保這些關系將被保留,并在Rectangle的寬度改變時自動調整高度和邊框寬度。
這種綁定的機制是使Qt Quick中的UI定義大多以聲明的方式編寫。綁定表達式(綁定的右側)可以任意復雜,包含對其他對象屬性的引用,甚至調用其他方法。
在Qt 5的生命周期中,我們已經看到,綁定使代碼的表現力更強,并刪除了很多需要編寫的膠水代碼。所以,在Qt 6中,我們的目標是允許作為一個C++開發者也能使用這種機制。
讓我們看看如何在C++中表達同樣的關系。下面是我們希望這樣一個Rectangle如何寫成一個C++類。
class Rectangle { public: Property<int> width; Property<int> height; Property<int> border; Rectangle() { height.setBinding(width); border.setBinding([this]() { return height / 10; }); } };
這定義了一個具有3個屬性的Rectangle類:width,height和border。然后,構造函數設置兩個綁定,一個綁定將高度綁定到寬度,另一個綁定將邊框綁定到高度的10%。
當我們著手進行Qt 6時,我們面臨的問題是我們是否可以以高效且高效的方式來實現這一目標。
綁定系統的目標
除了良好且易于使用的語法外,系統還需要滿足其他一些要求。
讓我們看一下新系統的實施方式以及我們如何實現上述目標。
簡單實施
讓我們從最簡單的方法開始,以實現支持我們正在尋找的功能的 QProperty類:
template <typename T> class QProperty { std::function<T()> binding = nullptr; T data; public: T value() const { if (binding) return binding(); return data; } void setValue(const T &newValue) { if (binding) binding = nullptr; data = newValue; } void setBinding(std::function<T> b) { binding = b; } };
上面的實現可能是實現支持綁定的QProperty類的最簡單方法。它基本上包含了屬性數據和一個有可能為空的綁定的函數指針。每當在屬性上設置了一個綁定,如果設置了一個綁定,屬性獲取器將總是執行綁定來檢索值。
然而這種實現有幾個嚴重的缺點,使得它不適合按原樣使用。最明顯的一個缺點就是性能會非常差,特別是當綁定依賴于其他屬性,而這些屬性本身也有綁定的時候。每次調用getter時都要評估這些綁定,會造成嚴重的性能問題。更糟糕的是,這可能會導致應用程序崩潰或死鎖,萬一一個綁定以某種方式引用回自己。
立即和延遲的綁定評估
所以我們確實需要一個更高級一點的設計。基本上有兩種可能的方法來避免每次調用setter時計算綁定的值。這兩種方法都涉及到將結果值緩存在數據中。此外,我們還需要記住一個綁定所依賴的屬性。
Qt Quick在Qt 5中做的就是即時綁定評估,這意味著每當一個屬性被改變,我們就會立即觸發對所有依賴這個屬性的綁定的重新評估。這個系統的缺點是,它可能會導致不必要的綁定表達式的評估。一個例子是一個被綁定為width*height的屬性區域。如果寬度和高度都被分配了新的值,面積就會被計算兩次,盡管只有第二個結果會被使用。
因此,在 Qt 6 中,我們使用了延遲綁定評估。這意味著我們遞歸地將所有依賴于屬性的綁定標記為 dirty。然后,屬性獲取器檢查該 dirty 標志,如果它為真,則重新評估綁定表達式,然后將結果存儲在數據中并清除 dirty 標志。
這就是QProperty現在的簡化視圖。
template <typename T> class QProperty { T val; QPropertyBindingData d; public: T value() const { if (d.hasBinding()) d.evaluateIfDirty(this); d.registerWithCurrenlyEvaluatingBinding(); return this->val; } void setValue(const T &t) { d.removeBinding(); if (this->val == t) return; this->val = t; notify(); } };
這里發生的事情是,getter檢查我們是否有一個綁定,如果有,則重新評估它。之后,作為第二步,它將自己與任何可能正在評估的綁定進行注冊。setValue()與之前相當類似。如果新舊值相同,我們就會快捷設置器,以避免這種情況下的綁定重新評估。如果設置了新的值,我們就調用notify(),而notify()又會將所有依賴于這個屬性的綁定標記為dirty。
還有很多細節需要我們去解決。例如,依賴注冊使用線程本地存儲來了解當前正在評估的綁定。如果你想知道所有的細節,請看Qt 6中QProperty的實現。
通知和變更處理程序
除了設置綁定外,QProperty還允許為屬性注冊變化處理程序。使用QProperty的onValueChanged()或subscribe()方法,可以注冊一個回調,每當屬性的底層值發生變化時,這個回調就會被調用。
當屬性的值通過調用setter而改變時,或者當屬性的綁定因為它的一個依賴關系改變而被標記為dirty時,回調將被調用。
QObjects屬性系統中的綁定支持回顧上面概述的目標,你可能已經注意到,QProperty的實現并沒有解決Qt 6中綁定引擎的所有目標。它的性能確實非常好(見下面進一步的性能數據),而且它只是在沒有使用綁定時增加了一個小的開銷。這個開銷主要是在getter中檢查我們是否有綁定和對當前正在評估的綁定進行TLS查找,在setter中快速檢查依賴關系。
但它確實給每個屬性帶來了不可忽視的額外4到8個字節的內存開銷,而且它也沒有和QObject中現有的屬性系統集成。接下來我們來看看這些是如何解決的。
雖然現在的QProperty可以獨立使用,也可以在任何類中使用,但我們希望有一個能與QObject中現有的屬性系統無縫集成、兼容的東西。這個系統是圍繞QObject的屬性建立的,只是在類定義中擁有一個setter和一個getter作為公共成員。這如何用數據來支持有些無關緊要。
為了支持這些屬性的數據綁定,我們需要看看如何調整QProperty的想法來適應這里。
我們最終得到的是一個實現屬性的QObject公共API的簡單擴展。
class MyObject : public QObject { Q_PROPERTY(int x GET x SET setX BINDABLE bindableX) // the line below was “int xData;” in Qt 5 Q_OBJECT_BINDABLE_PROPERTY(MyObject, int, xData) public: int x() { return xData; } void setX(int x) { xData = x; } QBindable<int> bindableX() { return &xData; } };
紅色標記的部分是Qt 6中的新內容。正如你所看到的那樣,在Qt 6中,使一個屬性可綁定所需的改動相對較少。簡單的用于存儲數據的 "int xData; "被一個實現綁定邏輯的宏所取代,即QProperty作為一個獨立類所做的一些事情。此外,我們增加了一個新的bindableX()方法,該方法返回一個QBindable<int>,并在Q_PROPERTY宏中告訴元對象系統。
QBindable<T>是一個輕量級接口,它提供了QProperty中也有的附加功能。它允許設置和檢索綁定并注冊通知。例如,在MyObject的x屬性上設置一個綁定可以通過調用來實現。
myObject-> bindableX()。setBinding([otherObject](){ return otherObject-> x()+ otherObject-> width(); }
使用這些宏以及我們知道QObject正在使用它的事實有兩個優點。與QProperty不同,Q_OBJECT_BINDABLE_PROPERTY不會增加任何內存開銷。宏實現的對象的大小與要存儲的數據的大小相同。這是通過將綁定數據移到整個QObject實例的公共數據結構(按需分配)中來實現的。
它使查找綁定的速度稍微慢一些,但是另一方面,由于在QObject中具有按需數據結構,因此我們可以避免對當前正在執行的綁定進行TLS查找。這也意味著,當不使用綁定程序對setter和getter進行指針查找和比較時,可以減少運行時開銷。
讓我們快速看一下它是如何實現的。為了允許在QObject屬性中使用綁定,上面的Q_OBJECT_BINDABLE_PROPERTY宏擴展為兩件事。首先,它在對象內部定義了一個靜態成員函數:
static constexpr size_t _qt_property_cData_offset() { return offsetof(MyObject, xData); }
然后,此方法允許被用作下一行中定義的QObjectBindableProperty實例的模板參數:
QObjectBindableProperty <MyObject,int,MyObject :: _qt_property_cData_offset> xData;
這樣做的結果是,我們現在有了一個方法,可以從屬性數據的this指針計算出擁有屬性數據的QObject的this指針。這個東西我們又用來從QObject中檢索一個QBindingStorage指針。這個指針可能是空的,在這種情況下,我們有快速路徑,在這個對象上沒有使用綁定。否則,我們在QBindingStorage中查找QProperty內置的QPropertyBindingData。一旦我們檢索到一個有效的綁定數據的指針,QObjectBindableProperty基本上就會進行和QProperty一樣的操作。
向后兼容
像Qt 5一樣使用changeSignal()作為通知實現的屬性將繼續像以前一樣工作。這意味著它們可以與Qt Quick中的綁定一起使用,但不能與C ++中的綁定一起使用。但是,他們還將繼續使用即時綁定評估。
為了獲得新系統的全部好處,您應該考慮將綁定支持添加到您自己的屬性中。這將使它們可以從C ++綁定,并且在大多數情況下將開始使用延遲綁定評估。向QObject的現有屬性添加綁定支持是100%向后兼容的。
Qt 6本身的大多數屬性仍未移植為也不支持新的綁定引擎。我們計劃在Qt 6.1和6.2中實現這一點。
基準數據
我們先來看看不使用綁定時屬性讀寫的性能。這一點很重要,因為我們不希望現有代碼出現較大的回歸。為了測試,我們看一個整數屬性。這測試的是最壞的情況,因為讀寫一個整數的速度是最快的,因此結果將最清楚地顯示任何增加的項。
讀 | 寫 | |
舊樣式屬性 | 3,8ns | 7.2ns |
QObjectBindableProperty(無通知) | 4,3ns | 4,5ns |
QObjectBindableProperty(信號已更改) | 4,3ns | 8.2ns |
QProperty | 9,1ns | 5,4ns |
表中顯示了結果,測試了幾個案例。第一個是用Qt 5的方式實現的一個屬性,有getter、setter和一個變化的信號。接下來的兩行使用Q_OBJECT_BINDABLE_PROPERTY使屬性可綁定。在一種情況下,我們沒有添加Qt 5風格的改變信號(因為新系統并不依賴它們),另一種情況下,為了向后兼容,仍然發出一個改變信號。最后一行顯示了QProperty的表現。
正如你所看到的,我們對于getter的速度慢了10%左右(但請注意,舊式屬性的getter擴展為一個包含三條指令的函數調用)。對于最常見的屬性沒有變化信號的情況,setter要快40%。QProperty稍微慢一些,因為它需要做一個TLS查找。
對于基于QString的屬性來說,差異會小得多,所以我們可以得出結論,在沒有使用綁定的情況下,我們成功地添加了對綁定的支持,而沒有顯著的開銷。
現在讓我們看看綁定的性能如何。為此,我們使用一個整數屬性與另一個整數屬性的簡單直接綁定。我們有兩個測試案例,一個案例是我們連續設置第一個屬性,然后讀取第二個屬性的值。在第二個案例中,我們只對第一個屬性進行寫入,但從不讀取第二個屬性。每一個案例,我們都分成兩個子案例,一個是我們通過QObjects通用屬性接口(setProperty()和property())讀寫值,一個是我們使用C++ setter和getter。
然后,我們為舊式屬性以及支持直接綁定的新屬性運行這些測試用例。
讓我們從一個用QML定義的綁定開始,并像在Qt 5中一樣進行評估。
訪問方式 | 寫讀 | 只寫 | 寫讀 | 只寫 |
setProperty /屬性 | 設置器/獲取器 | |||
舊樣式屬性 | 370ns | 240ns | 130ns | 130ns |
QObjectBindableProperty(無通知) | 370ns | 110ns | 120ns | 14ns |
QObjectBindableProperty(信號已更改) | 410ns | 120ns | 140ns | 25ns |
QProperty | 440ns | 130ns | 130ns | 10ns |
雖然Qt 5中的QML為某些選定的屬性提供了一些快捷方式,但某些屬性可能最終會通過QObject的通用屬性接口進行訪問。該表第一行中的數字反映了我們在Qt 5.15中可以獲得的最壞情況和最好情況。
其他行顯示了我們在Qt 6中可以獲得的性能。您會看到,在每次寫入之后都進行一次讀取的情況與Qt 5中的時間大致相同。這是可以預期的,因為我們需要對Qt 5進行處理。同樣的工作量。但是在所有情況下,在有多次寫入的情況下,在我們再次需要該屬性的值之前,新系統在一定程度上擊敗了舊系統。
讓我們看一下在C ++中設置綁定時會發生什么。由于舊的屬性系統無法做到這一點,因此我們在此處通過將lambda連接到設置了新值的更改信號來對其進行仿真。應該注意的是,這不能替代綁定,因為它根本無法擴展到更復雜的綁定表達式,并且需要大量的手動設置才能捕獲所有依賴項。
訪問方式 | 寫讀 | 只寫 | 寫讀 | 只寫 |
setProperty /屬性 | 設置器/獲取器 | |||
舊樣式屬性 | 230ns | 120ns | 29ns | 30ns |
QObjectBindableProperty(無通知) | 250ns | 100ns | 35ns | 12ns |
QObjectBindableProperty(信號已更改) | 280ns | 120ns | 51ns | 22ns |
QProperty | 300ns | 120ns | 48ns | 9ns |
最左邊的兩列主要供參考,并與上表進行比較。在C ++中,幾乎永遠不會通過基于字符串的通用屬性API訪問屬性。相反,最右邊的兩列反映了C ++中的典型用法。
可以看出,綁定系統的性能幾乎與兩個舊樣式屬性之間的直接信號/插槽連接一樣好。鑒于它要靈活得多,并且可以自動捕獲所有依賴項(需要使用信號/插槽手動聲明),因此數量很多。
您還可以看到,使用setter和getter的基于C ++的綁定比Qt 5.15中QML中定義的綁定快3-10倍。展望未來,我們計劃通過探索將QML中定義的綁定表達式編譯為C ++然后進行匯編的方式來利用這一事實。
結論
Qt 5中的綁定引擎使Qt Quick如此成功。有了Qt 6,我們現在已經把這個引擎從Qt Quick中移到了Qt的核心,并且讓它也能為C++開發者所用。
在這樣做的同時,我們成功地實現了比 Qt 5 中的性能的顯著提高。盡管如此,仍未完成工作,因為庫中的大多數屬性仍需要移植到新系統上。
好了這就是今天的內容了,如果今天的文章未解決你的需求,點擊獲取更多文章教程。不要忘了在評論與我們分享您的想法和建議。
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自: