深度解析qml引擎---(2)绑定(binding)
强烈的希望是人生中比任何欢乐更大的兴奋剂。——尼采
上一篇文章讲了QML引擎加载qml文件的过程,大体过程是,解析qml文件,然后为文件中的每个元素创建对应的c++对象。例如,qml文件中如果使用了Text类型,引擎会创建对应的QQuickText类的实例。
qml文件被引擎加载之后,在运行阶段,QML引擎并没有很多地参与, 事件处理和场景绘制是由C++类完成的。例如,qml的中的TextInput控件类型对应的c++类是QQuickTextInput,该控件的输入事件由QQuickTextInput::keyPressEvent() 处理,绘制由QQuickTextInput::updatePaintNode()处理,QML引擎并没有再参与。
在运行阶段,QML引擎仍然参与了两个事情: Bound signal handlers 和 property binding updates。例如,MouseArea控件的onClicked信号会与一个处理函数对应起来,这就是Bound signal handlers.
import QtQuick 2.0
Rectangle {
width: 300
height: 300
color: "lightsteelblue"
Text {
anchors.centerIn: parent
text: "Window Area: " + (parent.width * parent.height)
}
}
如上所示例子,它涵盖了两种类型的属性绑定:
1)简单的属性赋值(Simple value assignments)。 例如,QQuickRectangle的width属性被赋值为300. 这个赋值动作对应的VME instruction是 STORE_DOUBLE, 该指令会在创建c++对象时被执行,VME会调用QMetaObject::metacall(QMetaObject::WriteProperty, …),该函数最终调用QQuickRectangle::setWidth(). 设置完成后,QML引擎就不会在更改这个width属性值了。
2)属性绑定(Binding assignments)。 例如,上例中的,Text的text属性与其parent的width属性相关联,当其parent的width属性变化时,其text的值自动相应变化,内部时如何实现的呢,请看下面对binding的分析:
Creating the Binding
令QML_COMPILER_DUMP=1, 我们可以看到bytecode中包含的instructions如下所示(关于bytecode请参考上一篇文章),两种类型的绑定对应的instruction都是STORE_COMPILED_BINDING。
... 9 STORE_COMPILED_BINDING 43 1 0 10 FETCH 19 11 STORE_COMPILED_BINDING 17 0 1 ...
Compiled binding方式是一种优化绑定方式,在这片文章中我们先看普通的绑定,普通的绑定所对应的instruction是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:: Function对象并不会被执行,但是它会一直保留。
(传统的JavaScript引擎是把JavaScript代码先编译为字节码,然后再通过解释器执行字节码,V8引擎运用JIT技术,不通过解释器执行字节码,而是直接把JavaScript代码编译成运行在CPU(x86/x64/ARM)上的机器码)。
STORE_BINDING指令创建一个绑定可总结为:先创建了一个QQmlBinding对象,然后该对象借助V8引擎把传给它的JavaScript函数编译成了一个V8::Function对象。
Running the Binding
在某些时候,需要运行绑定的函数(上面讲的javaScript函数),这时V8引擎会运行绑定行数并将结果赋值给相应的属性。这些都是在creating阶段(创建c++对象的阶段)的最后阶段完成的,在最后阶段 QQmlVME::complete()会调用每个绑定对象的update()函数,在我们的例子中就是QQmlBinding:: update()函数。update()只是简单的执行v8:Function对象并将返回值赋给目标属性, 这在我们的例子中就是Rectangle的text属性。
但是V8引擎是怎么知道parent.width和parent.height的值的呢?实际上,它不知道。V8引擎没有任何线索知道到qml文件中包含了哪些QObject对象(每个qml基本类型对应的c++类都是继承自QObject类的),也不知道他们 之间的层次关系,也不知道每个对象都有哪些属性。
当V8引擎遇到一个未知类对象或未知属性时,它会询问QML引擎中的一个对象包裹器(Object Wrapper),这个对象包裹器会为它找到正确的类或属性,并把它们返回给V8引擎。下面我们通过堆栈信息来看一看QQuickItem的width属性是如何被访问的:
#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 ()
We can see that the wrapper is in qv8qobjectwrapper.cpp and ends up calling QObject::qt_metacall(QMetaObject::ReadProperty, …) to get the property value. The wrapper was called from v8 code, which itself was called by generated machine code of our v8::Function object. The generated machine code doesn’t have stack frames, and therefore GDB is unable to show the backtrace after the ??. I cheated a bit and pieced together this backtrace from two separate backtraces, which explains the inconsistent frame numbering.
从上面的堆栈信息来看,我们发现qv8qobjectwrapper.cpp中的包裹类最终调用函数QObject::qt_metacall(QMetaObject::ReadProperty,…) 来获取属性值。
So the v8 engine involves an object wrapper to get property values. In the same vein, it involves a context wrapper to find objects themselves, for example the parent object that is accessed during binding evaluation.
To sum up: A binding is evaluated by running the compiled v8::Function code. The v8 engine access unknown objects and properties by calling out to wrappers in Qt. The result returned by the v8::Function is then written to the target property.
Updating the Binding
好了,现在我们知道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信号,以便当属性改变时重新计算绑定的值。
Ref:
http://www.kdab.com/qml-engine-internals-part-2-bindings/
https://www.jianshu.com/p/9b277a3ee613