原文 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)。如需转载,无须我们授权,但需要注明原文链接(该文的链接),及原作者,谢谢!