原文 ?QML Engine Internals, Part 2: Bindings
譯者注:這個解析QML引擎的文章共4篇,分析非常透徹,在國內幾乎沒有找到類似的分析,為了便于國內的QT/QML愛好者和工作者也能更好的學習和理解QML引擎,故將這個系列的4篇文章翻譯過來。翻譯并不是完全直譯,有不足之處,請指正,謝謝!
———————————————————————————————————————————
上一篇:QML文件加載? ? ?下一篇 綁定類型
該博文是深入解析QML引擎系列博文中的第二篇。在上一篇博文中,我們已經為大家揭示了QML引擎是如何加載QML文件的。簡明扼要地回顧一下:解析QML文件,并為文件中的所有元素創建C ++對象。例如,QML文件中包含一個Text(文本)元素,QML引擎就會創建一個C ++ ?QQuickText類的實例。
QML引擎主要用于處理QML文件的加載,加載之后的運行時階段就不再那么需要它了。運行時的事件處理和繪制等都是由它生成的C++類來完成的。例如,TextInput(文本輸入)元素的輸入事件是由QQuickTextInput::keyPressEven()處理,繪制則由QQuickTextInput::updatePaintNode()實現,完全不需要QML引擎參與。
但是在運行時,QML引擎依然會涉及到兩個重要的東西:信號處理器和屬性綁定更新(譯者注:屬性綁定更新,其實就是計算屬性右邊表達式的值,后續有詳細的講解,這個地方不用擔心不明白。)。比如MouseArea的一個onClicked處理器就是信號處理器(譯者注:MouseArea是第一篇博文的例子中的一個元素,從第一篇分析的內容,我們了解onClicked這種這樣的信號處理器也被看作是屬性值,和普通的屬性沒啥差別)。我們將在這篇文章中深入分析綁定(bindings)。在此之前請先看下面這個例子:
在這個例子中,包含了給屬性賦值的兩種方式:
1. 簡單的賦一個值,比如給QQuickRectangle的width屬性賦值300。對應的VME指令是STORE_DOUBL。它會在組件創建后執行,只是簡單的調用函數QMetaObject::metacall(QMetaObject::WriteProperty,…), ?該函數最終執行QQuickRectangle:setWidth()來設置width屬性的值。在初始化之后,QML引擎再也不會修改width屬性的值了。
2. 賦一個綁定(binding),比如給text屬性賦一個綁定 "Window Area:" +(parent.width* parent.height),給anchors.centerIn屬性賦一個綁定 parent。綁定的神奇之處在于,當Rectangle的height和width屬性改變時,會自動更新到text屬性。這是如何實現的呢?其實也沒那么神奇,接下來我們將為你揭曉它的運作機制。(譯者注:原作者說的binding到底是什么,下面馬上就會揭曉,不用擔心。)
創建綁定
通過設置QML_COMPILER_DUMP=1來輸出VME指令,我們可以看到例子中的兩個綁定都是由指令STORE_COMPILED_BINDING創建的:
編譯后綁定是一種優化的綁定,我們還是先研究一下普通綁定,它是由STORE_BINDING指令創建的。查看QQmlVME::run()的代碼,我們發現代碼中創建了一個QQmlBinding對象,并把 "function $text() { return "Window Area:"+ (parent.width *parent.height) }" 做為它的表達式。沒錯,每一個綁定都是一個JavaScript函數!"function $text()" 這部分代碼是由QML編譯器添加的,這是因為QML的JavaScript引擎V8只支持完整的函數。這個JavaScript函數緊接著會被V8編譯器編譯成一個V8::Function對象。因為V8引擎有一個實時(JIT)編譯器,所以它會生成本地的機器碼(譯者注:傳統的JavaScript引擎是把JavaScript代碼先編譯為字節碼,然后再通過解釋器執行字節碼,V8引擎運用JIT技術,不通過解釋器執行字節碼,而是直接把JavaScript代碼編譯成運行在CPU(x86/x64/ARM)上的機器碼)。這時,V8:: Function對象并不會被執行,但是它會一直保留。
STORE_BINDING指令創建一個綁定可總結為:先創建了一個QQmlBinding對象,然后該對象借助V8引擎把傳給它的JavaScript函數編譯成了一個V8::Function對象。
(譯者注:為了更容易理解后續的內容,在這里約定“綁定”即JavaScript函數,“綁定對象”即QQmlBinding對象,計算綁定的值即表示運行JavaScript函數求值,或者執行V8::Function代碼求值)
運行綁定
在某些時候,綁定需要被運行,這意味著讓V8引擎對綁定求值并將結果賦值給對應的屬性。這些都是在創建階段的最后階段完成的。
QQmlVME::complete()會調用每個綁定對象的update()函數,在我們的例子中就是QQmlBinding:: update()函數。update()只是簡單的執行v8:Function對象并將返回值賦給目標屬性,這在我們的例子中就是Rectangle的text屬性。
但是V8引擎是怎么知道parent.width和parent.height的值的呢?說實在的,它究竟是怎么知道parent對象的?答案就是:它不知道。V8引擎沒有任何線索知道到底存在哪些對象,類名是什么,也不知道這些對象的屬性是什么。當V8引擎遇到一個未知類或未知屬性時,它會詢問QML引擎中的一個對象包裹器(Object Wrapper),這個對象包裹器會找到正確的類或屬性,并把他們返回給V8引擎。下面我們通過堆棧信息來看一看QQuickItem的width屬性是如何被訪問的:
從上面的堆棧信息來看,我們發現qv8qobjectwrapper.cpp中的包裹類最終調用函數QObject::qt_metacall(QMetaObject::ReadProperty,…) 來獲取屬性值。首先包裹類被V8代碼調用,然后V8代碼又被V8::Function對象對應的機器碼調用。由于機器碼沒有堆棧幀(stack frames),因此GDB(調試工具)沒法顯示在??之后的堆棧信息。在上面的堆棧信息中我做了一點點假,其實它是由兩個獨立的堆棧信息拼起來的,細心的讀者會發現,堆棧幀的序號并是不連續的。
由上可知,V8引擎會使用一個對象包裹類來獲取屬性值。同理,它會使用一個上下文包裹類來找到對象。例如,在我們的例子中,計算綁定值的過程中需要訪問parent對象,就是通過這種方式來找到parent的。
綜上所述:通過運行編譯后的V8::Function代碼來對綁定進行求值,再由V8引擎通過Qt里的包裹類來訪問對象和屬性,然后將求的值賦給目標屬性。
更新綁定
好了,現在我們知道text屬性是如何獲得它的初始值的。但是綁定更新是如何實現的?當height和width屬性改變時,QML引擎是怎么知道需要重新對綁定求值的呢?
這個問題的答案就隱藏在對象包裹類中。你應該還記得,當V8引擎需要訪問一個屬性時,就會調用它。這個對象包裹類不止返回屬性值:它還會捕獲所有被訪問過的屬性。從根本上講,當一個屬性被訪問時,對象包裹類會調用綁定對象的捕獲函數,在我們的例子中就是QQmlJavaScriptExpression::GuardCapture::captureProperty() ?(QQmlBinding是QQmlJavaScriptExpression的子類)。在捕獲函數內部實現中,只是簡單地把綁定對象的一個槽函數連接到被捕獲屬性的NOTIFY信號。當NOTIFY信號被觸發時,與之連接的槽函數就會被調用,并重新計算綁定的值。如果你還沒有聽說過NOTIFY信號,也不用擔心,這很簡單:當一個屬性用Q_PROPERTY來聲明時,在那里就可能聲明了一個NOTIFY信號。只要屬性發生改變,擁有該屬性的對象就會觸發NOTIFY信號。比如,QQuickItem的width屬性的聲明類似如下:
Q_PROPERTY(qrealwidth READ width WRITE setWidth NOTIFY widthChanged)
在我們這個例子中,首次運行綁定,訪問width屬性時,該屬性的捕獲函數將綁定對象中的一個槽函數連接到widthChanged()信號。在此之后,只要QQuickItem觸發widthChanged()信號,對應的槽函數將被調用,并重新計算綁定的值。
這就是為什么當你的屬性發生改變時,擁有并觸發NOTIFY信號是非常的重要。假如你忘了這樣做,綁定的值就不會被重新計算,基本上,屬性綁定就無法正確的運作。另一方面,盡管屬性并沒有真正地改變,但你也觸發了NOTIFY信號,那么綁定的值也會被毫無意義地重新計算。
綜上所述:當訪問屬性時,對象包裹類會調用綁定對象的捕捉函數,它會將綁定對象的一個槽函數連接到該屬性的NOTIFY信號,以便當屬性改變時重新計算綁定的值。
結論
在這篇博文中,我們已經深入分析綁定是如何工作的。總結成一句簡短的話就是:每個綁定都是一個編譯過的JavaScript函數,當任何一個引用的屬性改變時,它將重新被計算。
我希望你喜歡閱讀這些,我確信深入研究綁定的本質是非常有趣的。
在這個系列的下一篇博文中,我們將了解不同的綁定類型。現在,我們只研究了最基本的綁定,QQmlBinding,但我們知道還存在更多的綁定類型,比如編譯后綁定。它們神秘的面紗即將被揭開,敬請關注!
如果有什么疑問或者對QML應用和研究感興趣的朋友,歡迎加入我們進行討論(QQ群:280689979)。如需轉載,無須我們授權,但需要注明原文鏈接(該文的鏈接),及原作者,謝謝!
上一篇:QML文件加載? ? ?下一篇 綁定類型