- PVSM.RU - https://www.pvsm.ru -
Этот пост является продолжением данной [1] (в переводе [2]) статьи.
В предыдущем посте мы рассмотрели, как движок QML загружает файлы. Напомню, что сначала QML-файлы разбираются парсером, затем компилируются в промежуточный байт-код и наконец следуя инструкция байт-кода, для каждого эемента в каждом QML-файле создаётся C++ объект. Например, мы видели, что когда QML файл содержит элемент Text, то движок создает экземпляр C++ класса QQuickText.
На самом деле, загрузка файлов — это почти всё, чем занимается движок QML. После загрузки, он перестаёт вмешиваться в процесс работы приложения. Обработка событий и отрисовка элементов в Runtime полностью ложится на плечи C++. Например, элемент TextInput обрабатывает события вроде QQuickTextInput::keyPressEvent() и обновления QQuickTextInput::updatePaintNode() [3], без участия движка QML.
Но существуют две важные вещи, на которые движок QML всё же влияет во время выполнения: Связанные обработчики сигналов (Bound signal handlers) и обновление привязок свойств (property binding). Связанные обработчики сигналов — это такие вещи, как onClicked обработчик для MouseArea. Сегодня же мы рассмотрим привязки (Bindings).
Пример:
import QtQuick 2.0
Rectangle {
width: 300
height: 300
color: "lightsteelblue"
Text {
anchors.centerIn: parent
text: "Window Area: " + (parent.width * parent.height)
}
}
Данный пример содержит два вида присвоения свойства:
Если мы выставим параметр QML_COMPILER_DUMP=1, то мы увидим, что биндинги создаются инструкцией STORE_COMPILED_BINDING:
...
9 STORE_COMPILED_BINDING 43 1 0
10 FETCH 19
11 STORE_COMPILED_BINDING 17 0 1
...
Скомпилированные привязки (Compiled bindings), это на самом деле оптимизированный вариант. Давайте для начаа взглянем на нормальные привязки, которые создаются инструкцией STORE_BINDING. Функция QQmlVME::run() [4] создаёт объект QQmlBinding [5], которому передаёт строку вида
function $text() { return “Window Area: ” + (parent.width * parent.height) }
являющуюся JS-выражением.
Именно так, каждая привязка является JavaScript-функцией. Часть функции “function $text()” добавляется компилятором QML, таким образом так как V8 [6], являющийся JS-движком QML, может, как несложно догадаться, выполнять только JS-код. Строковая функция затем компилируется в объект v8::Function, компилятором v8. Движок v8, за счёт встроенного JIT-компилятора [7], создаёт нативный код. Он не выполняет созданный объект v8::Function, но сохраняет его на будущее.
Итого: При выполнении инструкции STORE_BINDING, создаётся объект QQmlBinding, который компилирует переданную ему в виде строки функцию в объект v8::Function, используя для этого встроенный JS-движок V8.
В какой-то момент, нашу привязку будет необходимо выполнить, а это значит что V8 должен будет выполнить переданную ему функцию с телом привязки и вернуть результат выполнения для присвоения его целевому свойству. Данный вызов происходит в первую очередь в конце фазы создания, функция QQmlVME::complete() [8] последовательно вызывает метод update() для каждой привязки. В нашем случае, вызывается функция QQmlBinding::update() [9]. Метод update() просто выполняет содержимое объекта v8::Function и записывает результат выполнения в свойство text нашего прямоугольника.
Но погодите-ка, а как V8 узнаёт о значении переменных parent.width и parent.height? Действительно, откуда он узнаёт о родительском объекте? Ответ: V8 понятия не имеет ни о каких объектах типа QObject, представленных в QML файле, как и о составе их свойств. Когда V8 встречается с неизвестным ему объектом или неизвестным свойством какого-либо объекта, он задаёт вопрос wrapper -объекту движка QML, который находит подходящий объект или свойство и передаёт его (или его значение) обратно в V8. Давайте посмотрим, как происходит доступ к свойству width, объекта QQuickItem на примере данного дампа:
#0 QQuickItem::width (this=0x6d8580) at items/qquickitem.cpp:4711
#1 0x00007ffff78e592d in QQuickItem::qt_metacall (this=0x6d8580, _c=QMetaObject::ReadProperty, _id=8, _a=0x7fffffffc270) at .moc/debug-shared/moc_qquickitem.cpp:675
#2 0x00007ffff7a61689 in QQuickRectangle::qt_metacall (this=0x6d8580, _c=QMetaObject::ReadProperty, _id=9, _a=0x7fffffffc270) at .moc/debug-shared/moc_qquickrectangle_p.cpp:526
#3 0x00007ffff7406dc3 in ReadAccessor::Direct (object=0x6d8580, property=..., output=0x7fffffffc2c8, n=0x0) at qml/v8/qv8qobjectwrapper.cpp:243
#4 0x00007ffff7406330 in GenericValueGetter (info=...) at qml/v8/qv8qobjectwrapper.cpp:296
#5 0x00007ffff49bf16a in v8::internal::JSObject::GetPropertyWithCallback (this=0x363c64f4ccb1, receiver=0x363c64f4ccb1, structure=0x1311a45651a9, name=0x3c3c6811b7f9) at ../3rdparty/v8/src/objects.cc:198
#6 0x00007ffff49c11c3 in v8::internal::Object::GetProperty (this=0x363c64f4ccb1, receiver=0x363c64f4ccb1, result=0x7fffffffc570, name=0x3c3c6811b7f9, attributes=0x7fffffffc5e8)
at ../3rdparty/v8/src/objects.cc:627
#7 0x00007ffff495c0f1 in v8::internal::LoadIC::Load (this=0x7fffffffc660, state=v8::internal::UNINITIALIZED, object=..., name=...) at ../3rdparty/v8/src/ic.cc:933
#8 0x00007ffff4960ff5 in v8::internal::LoadIC_Miss (args=..., isolate=0x603070) at ../3rdparty/v8/src/ic.cc:2001
#9 0x000034b88ae0618e in ?? ()
...
[more ?? frames from the JIT'ed v8::Function code]
...
#1 0x00007ffff481c3ef in v8::Function::Call (this=0x694fe0, recv=..., argc=0, argv=0x0) at ../3rdparty/v8/src/api.cc:3709
#2 0x00007ffff7379afd in QQmlJavaScriptExpression::evaluate (this=0x6d7430, context=0x6d8440, function=..., isUndefined=0x7fffffffcd23) at qml/qqmljavascriptexpression.cpp:171
#3 0x00007ffff72b7b85 in QQmlBinding::update (this=0x6d7410, flags=...) at qml/qqmlbinding.cpp:285
#4 0x00007ffff72b8237 in QQmlBinding::setEnabled (this=0x6d7410, e=true, flags=...) at qml/qqmlbinding.cpp:389
#5 0x00007ffff72b8173 in QQmlBinding::setEnabled (This=0x6d7448, e=true, f=...) at qml/qqmlbinding.cpp:370
#6 0x00007ffff72c15fb in QQmlAbstractBinding::setEnabled (this=0x6d7448, e=true, f=...) a /../../qtbase/include/QtQml/5.0.0/QtQml/private/../../../../../../qtdeclarative/src/qml/qml/qqmlabstractbinding_p.h:98
#7 0x00007ffff72dcb14 in QQmlVME::complete (this=0x698930, interrupt=...) at qml/qqmlvme.cpp:1292
#8 0x00007ffff72c72ae in QQmlComponentPrivate::complete (enginePriv=0x650560, state=0x698930) at qml/qqmlcomponent.cpp:919
#9 0x00007ffff72c739b in QQmlComponentPrivate::completeCreate (this=0x698890) at qml/qqmlcomponent.cpp:954
#10 0x00007ffff72c734c in QQmlComponent::completeCreate (this=0x698750) at qml/qqmlcomponent.cpp:947
#11 0x00007ffff72c6b2f in QQmlComponent::create (this=0x698750, context=0x68ea30) at qml/qqmlcomponent.cpp:781
#12 0x00007ffff79d4dce in QQuickView::continueExecute (this=0x7fffffffd2f0) at items/qquickview.cpp:445
#13 0x00007ffff79d3fca in QQuickViewPrivate::execute (this=0x64dc10) at items/qquickview.cpp:106
#14 0x00007ffff79d4400 in QQuickView::setSource (this=0x7fffffffd2f0 at items/qquickview.cpp:243
#15 0x0000000000400d70 in main ()
Мы видим, что wrapper в файле qv8qobjectwrapper.cpp [10] вызывает функцию QObject::qt_metacall(QMetaObject::ReadProperty, …), для получения значения требуемого свойства. Wrapper (Объект-обёртка) был вызван из скомпилированного V8 кода, хранящегося в V8::Function. Сгенерированный машинный код к сожалению не имеет стека вызовов, поэтому GDB не в состоянии показать что творится за ??. Я вас немного обманул и показал выше два разных стека вызовов, что немного объясняет разрыв в нумерации строк.
Ещё раз: V8 использует объект-обёртку (object wrapper) для получения значений свойств. Таким же образом он использует обёртку-контекст (context wrapper), позволяющий искать сами объекты. например — родительский объект к которому мы обращаемся во время выполнения привязки.
Итак, привязка выполняется запуском кода V8::Function. Движок V8 получает доступ к неизвестным объектам и свойствам вызовом обёрток из Qt. Результат выполнения V8::Function записывается в целевое свойство.
Хорошо, мы теперь знаем как свойство text получило своё первоначальное значение. А как насчёт его обновления? Откуда движок QML узнаёт о том, что нужно обновить данное свойство при изменении высоты или ширины родительского прямоугольника?
Ответ на данный вопрос живёт в в том же самом объекте-обёртке (object wrapper), который, как Вы помните вызывается когда V8 требуется получить доступ к свойству. Наша обёртка делает немного больше, чем просто возвращает значение свойства: она записывает все свойства, к которым был запрошен доступ. По существу, когда происходит доступ к свойству, обёртка вызывает функцию захвата привязки, работающей в данный момент. В нашем примере, это QQmlJavaScriptExpression::GuardCapture::captureProperty() [11] (QQmlBinding — это подкласс класса QQmlJavaScriptExpression).
В функции захвата, привязка просто напросто присоединяется в сигналу типа NOTIFY свойства, которое было запрошено. Теперь, когда будет вызван NOTIFY-сигнал, будет вызван подключенный к нему слот привязки и сама привязка будет перезапущена. Если вы никогда не слышали о NOTIFY-сигнале, не волнуйтесь, его логика проста: Когда свойство определено с использованием макроса Q_PROPERTY, то в этом месте всегда будет присутсвовать сигнал NOTIFY, который будет высылаться всегда, когда будет изменяться данное свойство.
Для примера, вот декларация свойства width в классе QQuickItem:
Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged)
В нашем сценарии: когда к свойству width обратятся впервые, во время первого запуска нашей привязки, функция захвата свойства, соединит сигнал widthChanged() и слот запуска нашей привязки. Теперь, когда во время работы приложения объект QQuickItem эмитирует сигнал widthChanged(), все присоединённые к нему привязки будут вызваны и перезапущены.
Очень важно иметь NOTIFY-сигнал в самостоятельно определяемых свойствах и высылать его всегда, когда изменяется наше свойство. Если Вы забудете сделать это, то привязка не будет перезапущена и соответственно, привязка свойства не будет работать корректно. С другой стороны, если NOTIFY-сигнал будет выслан в момент, когда свойство не было изменено, привязка сработает вхолостую.
Подведём итог: Когда происходит доступ к свойству, объект-обёртка вызывает функцию захвата из привязки, которая присоединяет эту привязку к NOTIFY-сигналу свойства, чтобы при изменении свойства привязка выполнялась заново…
Сегодня мы рассмотрели то, как работают привязки в QML. Кратко: привязка — это JS-функция, которая выполняется каждый раз когда изменяются задействованные в ней свойства.
Я надеюсь Вам была интересна эта статья, мне например было очень интересно изучить внутреннюю логику работы привязок.
В следующей своей заметке мы рассмотрим различные типы привязок. Всё что мы рассмотрели сегодня — это базовая привязка, или QQmlBinding, но мы знаем, что существуют и другие типы, например скопилированные привязки. Их тайну мы разгадаем в ближайшее время, следите за обновлениями!
Автор: vitaly_KF
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/qt-2/13764
Ссылки в тексте:
[1] данной: http://www.kdab.com/category/blogs/qmlengineseries/
[2] в переводе: http://habrahabr.ru/post/150064/
[3] QQuickTextInput::updatePaintNode(): http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/quick/items/qquicktextinput.cpp#line1752
[4] QQmlVME::run(): http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/qml/qml/qqmlvme.cpp#line804
[5] QQmlBinding: http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/qml/qml/qqmlbinding_p.h#line74
[6] V8: http://code.google.com/p/v8/
[7] JIT-компилятора: http://en.wikipedia.org/wiki/Just-in-time_compilation
[8] QQmlVME::complete(): http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/qml/qml/qqmlvme.cpp#line1263
[9] QQmlBinding::update(): http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/qml/qml/qqmlbinding.cpp#line246
[10] qv8qobjectwrapper.cpp: http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/qml/qml/v8/qv8qobjectwrapper.cpp
[11] QQmlJavaScriptExpression::GuardCapture::captureProperty(): http://qt.gitorious.org/qt/qtdeclarative/blobs/master/src/qml/qml/qqmljavascriptexpression.cpp#line210
Нажмите здесь для печати.