原文?QML Engine Internals, Part 3: Binding Types
譯者注:這個解析QML引擎的文章共4篇,分析非常透徹,在國內幾乎沒有找到類似的分析,為了便于國內的QT/QML愛好者和工作者也能更好的學習和理解QML引擎,故將這個系列的4篇文章翻譯過來。翻譯并不是完全直譯,有不足之處,請指正,謝謝!
———————————————————————————————————————————
這篇博文是深入解析QML引擎系列博文的第三篇。在上一篇博文中,我們揭示了QML引擎中的綁定是如何運作的。在這篇文章中,我們將深入了解不同的綁定類型。某些內容是我在開發者日對話QtQuick Under the Hood中講過的。除此之外,這篇博文中將涵蓋一些新的內容。
簡要回顧
在回顧之前,讓我們快速地瀏覽一個簡單的綁定:
每一個像這樣的綁定實際上是一個JavaScript函數,運行時由V8引擎執行。執行的結果就是函數的返回值,然后將它設置給文本屬性。由于V8并不知道Qt對象和屬性,當遇到一個對象(如parent)或一個屬性(如width)時,它就請求QML中的上下文包裹類和對象包裹類去解析它們。當一個綁定被執行時,這些包裹類會記錄那些被訪問了的屬性,可以自動將每個屬性的改變信號(例如widthChanged())連接到一個可以重新執行綁定的槽函數。
現在我們已經重新溫習了一遍綁定的工作原理,讓我們趁熱打鐵,繼續分析不同的綁定方式。
綁定方式
在上一篇文章中,我指出每一個綁定都被解析成一個QQmlBinding對象的實例。這其實是一個哄騙孩子的謊言。如果每一個綁定都由QQmlBinding表示,則開銷會非常大。一個典型的QML應用,即使沒有上千個綁定,至少也有成百個綁定,所以需要讓每一個綁定更加輕量級。此外,當加載一個QML文件時,每一個綁定都是單獨編譯的。因此在加載過程中會多次調用V8編譯器,給系統造成不小的開銷。
QV8Bindings
為了解決QQmlBinding造成的開銷問題,使用了另外一個綁定類,取了一個容易混淆的名字:QV8Bindings。QV8Bindings內部用了一個數組來存放QML文件中的所有綁定,綁定用更加輕量級的QV8Bindings::Binding結構體來表示。QML的開發者過去花了很大力氣去減少這種結構的內存占用,他們甚至發現指針的最后兩位因為對齊的關系而沒有被使用。然后喪心病狂地利用這些空間去保存標志位,最終做到一個QV8Bindings::Binding只占用了64個字節。
QV8Bindings和QQmlBinding相比,有一個大優勢是,所有綁定都是在一起編譯的,所以只需要調用一次V8編譯器。在QQmlCompiler ::completeComponentBuild()函數中,你會發現,在編譯QML文件時,所有的綁定函數會組成一個大的JavaScript程序,并存儲在QQmlCompiledData(用于包含QML文件中所有類型的編譯數據)。當QML文件第一次實例化時,QV8Bindings::QV8Bindings()將對綁定程序進行編譯,編譯后保存在QQmlCompiledData中,然后將源代碼丟棄。當再次實例化相同的QML文件時,QML引擎將直接使用QQmlCompiledData中已編譯的綁定程序,并不需要再編譯一次它們。然而QQmlBinding卻不是這樣,每次實例化QML文件都需要執行一次編譯。
小結:因為QV8Bindings把QML文件中所有的綁定組織在一起,所以可以花費更少的內存,并只執行一次編譯。
那為什么我們不拋棄QQmlBinding?這個類為什么還依舊存在呢?某些情況下,綁定是不可共享的,例如它們使用了閉包或者使用了eval()函數。在這種情況下,每個綁定函數需要不同的上下文。因此不能和具有相同上下文的其他綁定一起編譯。因此在這種特殊情況下,將會使用QQmlBinding來表示綁定。當編譯一個QML文件時,是由QQmlCompiler::completeComponentBuild()來判定采用哪種綁定方式。另外,SharedBindingTester會檢測綁定應該用QV8Bindings,還是QQmlBinding。SharedBindingTester就是一個JS AST的訪問者。如果你查看一下代碼,你會發現SharedBindingTester也會測試哪些綁定是安全的,同時在QML文件初始化時避免多次執行綁定,源代碼的提交信息做了最好的描述。
為了讓QML代碼更加的簡潔,QQmlBinding和QV8Bindings::Binding都從QQmlAbstractBinding繼承。
QV4Bindings
假如你看過一些QML引擎的代碼,你很可能已注意到QV4Bindings類,這個類也是QQmlAbstractBinding的子類。它是另一個綁定類型嗎?和什么有關呢?與QV8Bindings相同的是,它也是QML文件中所有綁定的集合。不同的是,QV4Bindings只保存所謂優化過的綁定,也有人錯誤和混淆地稱之為編譯過的綁定。有一些綁定是可以被優化的,它們會用QV4Bindings表示,有一些綁定不能被優化,它們會用QV8Bindings來表示。
那么這個優化是什么呢?QV4Bindings并不由V8引擎執行,它會被編譯成字節碼,通過一個字節碼解析器執行。這個字節碼編譯器和解析器無法處理所有的JavaScript表達式,因為不可能提前編譯所有的JavaScript。
但是為什么使用字節碼呢?V8引擎會編譯成機器碼,難道不比一個字節碼解析器快嗎?結果證明它真的沒有字節碼解析器快,V8引擎執行綁定時,需要調用QML來解析對象和屬性,這個處理需要很大的開銷。另外當一個函數被多次調用時,V8引擎可能會在比較繁忙的情況下重新編譯一個函數,以此做更多地優化。對于QML的情況而言,所有這些處理都會造成很大的開銷,因為QML通常包含很多只有一句代碼的綁定。這里有一個我為開發者日準備的基準測試結果。在測試中,我只是簡單的讓QML引擎執行一個綁定幾百次。這是一個可以讓V4引擎輕松處理的簡單綁定。為了和V8引擎比較,設置環境變量QML_DISABLE_OPTIMIZER=1來完全禁用V4綁定。
如你所見,在這種特定情形下,V4字節碼引擎的確比V8快多了。
從本質上說,V4就是一個寄存器機器。和CPU相同的是,它具有的寄存器,用來存儲臨時值。不同的是,它不會從內存加載和儲存值——它從類的屬性加載和儲存值。設置環境變量QML_BINDINGS_DUMP=1,讓我們看一個簡單的綁定:
其指令輸出是:
如你所見,屬性width和height被加載到寄存器0和1中,然后這些寄存器乘起來,把結果保存在文本屬性中(文本屬性在QQuickText類中的屬性編號是42)。FetchAndSubscribe指令不僅加載屬性,也會監聽它的改變信號,從而實現綁定的自動更新。從上文的"匯編"代碼中,你還可以發現另一個優勢:V4編譯器是在編譯時解析對象和屬性,并將屬性的索引保存在字節碼中。所以在運行時,就可以直接通過索引訪問屬性,不用通過屬性名字進行查找。而V8引擎則需要調用QML對象和上下文包裝器來解析對象和屬性,這當然會產生更多的開銷。但是缺點是,V4引擎無法處理動態對象,例如那些通過setContextProperty()從C++導出的對象。如果綁定中含這種動態對象,則需要使用QV8Binding。
總結
歸納起來,有3個綁定類型,都是從QQmlAbstractBinding繼承:
1. QV4Bindings::Binding
2. QV8Bindings::Binding
3. QQmlBinding
QV4Bindings是最快的,因為其使用了自定義的字節碼引擎。QV8Bindings和QQmlBinding都是使用V8 JS引擎執行,但QV8Bindings將所有的綁定組織在一起,一次性編譯,然而QQmlBindings會在每個QML組件實例化過程中一個一個地進行編譯。
這有一個展示所有綁定類型的(沒啥用的)例子:
設置環境變量QML_COMPILER_DUMP=1,你會看到QML編譯器使用了兩次STORE_COMPILED_BINDING,一次STORE_V8_BINDING和一次STORE_BINDING。
STORE_BINDING為QQmlBinding,它用于font.pointSize,因為綁定使用了eval(),因此不可以被共享。
anchors.centerIn的綁定和文字都是V4綁定(STORE_COMPILED_BINDING指令,QV4Bindings:: Binding類)。
最后,font.wordSpacing是一個普通的QV8Bindings::Binding(STORE_V8_BINDING指令)。V4的字節碼編譯器和解析器應對三元運算符完全沒有問題,但是求補運算尚未實現,所以QML編譯器選擇使用V8綁定。
在這個系列的下一篇博文中,我們將嘗試自定義解析器。如果有什么疑問或者對QML應用和研究感興趣的朋友,歡迎加入我們進行討論(QQ群:280689979)。如需轉載,無須我們授權,但需要注明原文鏈接(該文的鏈接),及原作者,謝謝!