QObject 实现了这么多功能,那么,它是如何做到的呢?让我们通过它的 Source Code 来解开这个秘密吧。 QObject类的实现文件一共有四个.
1、qobject.h
QObject class 的基本定义,也是我们一般定义一个类的头文件.
2、qobject.cpp
QObject class 的实现代码基本上都在这个文件.
3、 qobjectdefs.h
这个文件中最重要的东西就是定义了 QMetaObject class,这个class是为了实现 signal、slot、properties,的核心部分。
4、qobject_p.h
这个文件中的 code 是辅助实现 QObject class 的,这里面最重要的东西是定义了一个 QObjectPrivate 类来存储 QOjbect 对象的成员数据。
理解这个 QObjectPrivate class 又是我们理解 QT kernel source code 的基础,这个对象包含了每一个 QT 对象中的数据成员,好了,让我们首先从理解 QObject 的数据存储代码开始我们的 QT Kernel Source Code 之旅。
二:元对象系统(Meta-Object System)
在使用 QT 开发的过程中,大量的使用了 signal 和 slot. 比如,响应一个 button 的 click 事件,我们一般都写如下的代码:
class MyWindow : public QWidget {
Q_OBJECT
public:
MyWindow(QWidget* parent) : QWidget(parent) {
QPushButton* btnStart = new QPushButton(“start”, this);
connect(btnStart, SIGNAL(clicked()), this, SLOT(slotStartClicked())); }
private slots:
void slotStartClicked(); };
void MyWindow:: slotStartClicked()
{
// 省略
}
这就是 QT 中的元对象系统(Meta Object System)的作用,为了更好的理解它,让我先来对它的功能做一个回顾,让我们一起来揭开它神秘的面纱。
三、Meta-Object System 的基本功能
Meta Object System 的设计基于以下几个基础设施:
1、 QObject 类
作为每一个需要利用元对象系统的类的基类 ,Q_OBJECT 宏, 定义在每一个类的私有数据段,用来启用元对象功能,比如,动态属性,信号和槽 。元对象编译器moc (the Meta Object Complier), moc 分析C++源文件,如果它发现在一个头文件(header file)中包含Q_OBJECT 宏定义,然后动态的生成另外一个C++源文件,这个新的源文件包含 Q_OBJECT 的实现代码,这个新的 C++ 源文件也会被编译、链接到这个类的二进制代码中去,因为它也是这个类的完整的一部分。通常,这个新的C++ 源文件会在以前的C++ 源文件名前面加上 moc_ 作为新文件的文件名。其具体过程如下图所示:
Moc分析时,需要判断源代码中的Q_OBJECT、Q_PROPERTY、Q_ENUMS、Q_CLASSINFO、slots、slot、emit等信息,并生成元对象;但这些关键字大部分C++编译器并不认识(Q_OBJECT除外),他们都被define为空。
Qt的信号槽机制其实就是按照名称查表
除了提供在对象间进行通讯的机制外,元对象系统还包含以下几种功能:
QObject::metaObject() 方法
它获得与一个类相关联的 meta-object
QMetaObject::className() 方法 ,在运行期间返回一个对象的类名,它不需要本地C++编译器的RTTI(run- time type information)支持
QObject::inherits() 方法
它用来判断生成一个对象类是不是从一个特定的类继承出来,当然,这必须是在QObject 类的直接或者间接派生类当中
QObject::tr() and QObject::trUtf8() 这两个方法为软件的国际化翻译字符串
QObject::setProperty() and QObject::property() 这两个方法根据属性名动态的设置和获取属性值
除了以上这些功能外,它还使用qobject_cast()方法在QObject类之间提供动态转换,qobject_cast()方法的功能类似于标准 C++的dynamic_cast(),但是qobject_cast()不需要RTTI的支持,
在一个QObject类或者它的派生类中,我们可以不定义Q_OBJECT宏。如果我们在一个类中没有定义Q_OBJECT宏,那么在这里所提到的相应的功能在这个类中也不能使用,从meta-object的观点来说,一个没有定义Q_OBJECT宏的类与它最接近的那个祖先类是相同的,那就是说,QMetaObject::className()方法所返回的名字并不是这个类的名字,而是与它最接近的那个祖先类的名字。所以,我们强烈建议,任何从QObject继承出来的类都定义Q_OBJECT 宏。
三:元对象编译器 - Meta Object Compiler (moc)
元对象编译器用来处理QT的C++扩展,moc 分析C++源文件,如果它发现在一个头文件(header file)中包含Q_OBJECT 宏定义,然后动态的生成另外一个C++源文件,这个新的源文件包含 Q_OBJECT 的实现代码,这个新的 C++源文件也会被编译、链接到这个类的二进制代码中去,因为它也是这个类的完整的一部分。通常,这个新的C++ 源文件会在以前的C++源文件名前面加上 moc_ 作为新文件的文件名。 如果使用qmake工具来生成Makefile文件,所有需要使用moc的编译规则都会给自动的包含到Makefile文件中,所以对程序员来说不需要直接的使用moc
除了处理信号和槽之外,moc还处理属性信息,Q_PROPERTY()宏定义类的属性信息,而Q_ENUMS()宏则定义在一个类中的枚举类型列表。 Q_FLAGS()宏定义在一个类中的flag枚举类型列表,Q_CLASSINFO()宏则允许你在一个类的meta信息中插入name/value 对。 由moc所生成的文件必须被编译和链接,就象你自己写的另外一个C++文件一样,否则,在链接的过程中就会失败。
Code example:
class MyClass : public QObject {
Q_OBJECT
Q_PROPERTY(Priority priority READ priority WRITE setPriority) Q_ENUMS(Priority)
Q_CLASSINFO("Author", "Oscar Peterson") Q_CLASSINFO("Status", "Active")
public:
enum Priority { High, Low, VeryHigh, VeryLow };
MyClass(QObject *parent = 0); virtual ~MyClass();
void setPriority(Priority priority); Priority priority() const; };
QT 对象已经包含了许多预定义的 signal,但我们总是可以在派生类中添加新的 signal。
QT 对象中也已经包含了许多预定义的 slog,但我们可以在派生类中添加新的 slot 来处理我们感兴趣的 signal signal 和 slot 机制是类型安全的,signal 和 slot必须互相匹配(实际上,一个solt的参数可以比对应的signal的参数少,因为它可以忽略多余的参数)。signal 和 slot是松散的配对关系,发出signal的对象不关心是那个对象链接了这个signal,也不关心是那个或者有多少slot链接到了这个 signal。QT的signal 和 slot机制保证了,如果一个signal和slot相链接,slot会在正确的时机被调用,并且是使用正确的参数。Signal和slot都可以携带任何数量和类型的参数,他们都是类型安全的。
所有从QObject直接或者间接继承出来的类都能包含信号和槽,当一个对象的状态发生变化的时候,信号就可以被emit出来,这可能是某个其它的对象所关心的。这个对象并不关心有那个对象或者多少个对象链接到这个信号了,这是真实的信息封装,它保证了这个对象可以作为一个软件组件来被使用。
槽(slot)是用来接收信号的,但同时他们也是一个普通的类成员函数,就象一个对象不关心有多少个槽链接到了它的某个信号,一个对象也不关心一个槽链接了多少个信号。这保证了用QT创建的对象是一个真实的独立的软件组件。
一个信号可以链接到多个槽,一个槽也可以链接多个信号。同时,一个信号也可以链接到另外一个信号。
所有使用了信号和槽的类都必须包含 Q_OBJECT 宏,而且这个类必须从QObject类派生(直接或者间接派生)出来, 当一个signal被emit出来的时候,链接到这个signal的slot会立刻被调用,就好像是一个函数调用一样。当这件事情发生的时候,signal和slot机制与GUI的事件循环完全没有关系,当所有链接到这个signal的slot执行完成之后,在 emit 代码行之后的代码会立刻被执行。当有多个slot链接到一个signal的时候,这些slot会一个接着一个的、以随机的顺序被执行。
Signal代码会由 moc 自动生成,开发人员一定不能在自己的C++代码中实现它,并且,它永远都不能有返回值。 Slot其实就是一个普通的类函数,并且可以被直接调用,唯一特殊的地方是它可以与signal相链接。
C++的预处理器更改或者删除 signal, slot, emit 关键字,所以,对于C++编译器来说,它处理的是标准的C++源文件。
五:Meta Object Class
前面我们介绍了Meta Object的基本功能,和它支持的最重要的特性之一:Signal & Slot的基本功能。现在让我们来进入 Meta Object 的内部,看看它是如何支持这些能力的。
Meta Object 的所有数据和方法都封装在一个叫QMetaObject 的类中。它用来查询一个 QT 类的 meta 信息,meta信息包含以下几种,:
1、信号表(signal table),其中有这个对应的 QT 类的所有Signal的名字
2、 槽表(slot table),其中有这个对应的QT类中的所有Slot的名字。
3、类信息表(class info table),包含这个QT类的类型信息
4、属性表(property table),其中有这个对应的QT类中的所有属性的名字。
5、指向parent meta object的指针(pointers to parent meta object)
QMetaObject 对象与 QT 类之间的关系:
每一个QMetaObject对象包含了与之相对应的一个QT类的元信息
每一个QT类(QObject 以及它的派生类) 都有一个与之相关联的静态的(static) QMetaObject 对象(注:class的定义中必须有 Q_OBJECT 宏,否则就没有这个Meta Object)
每一个 QMetaObject 对象保存了与它相对应的 QT 类的父类的 QMetaObject 对象的指针。或者,我们可以这样说:“每一个QMetaObject对象都保存了一个其父亲(parent)的指针”.注意:严格来说,这种说法是不正确的,最起码是不严谨的。
Q_OBJECT宏
Meta Object 的功能实现,这个宏立下了汗马功劳。首先,让我们来看看这个宏是如何定义的:
#define Q_OBJECT \
public: \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
struct QPrivateSignal {}; \
这里,我们先忽略Q_OBJECT_CHECK 和QT_TR_FUNCTIONS 这两个宏。
我们看到,首先定义了一个静态类型的类变量staticMetaObject,然后有一个获取这个对象指针的方法metaObject()。这里最重要的就是类变量staticMetaObject 的定义。这说明所有的 QObject 的对象都会共享这一个staticMetaObject 类变量,靠它来完成所有信号和槽的功能,所以我们就有必要来仔细的看看它是怎么回事了。
struct Q_CORE_EXPORT QMetaObject
{
//...................
struct { // private data
const QMetaObject *superdata;
const QByteArrayData *stringdata;
const uint *data;
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall;
const QMetaObject * const *relatedMetaObjects;
void *extradata; //reserved for future use
} d;
}
那么每一个QObject类(或其派生类)可能有一个父类,或者父类的父类,或者很多的继承层次之前的祖先类。或者没有父类(QObject)。那么 superdata 这个变量就是指向与其最接近的祖先类中的QMetaObject对象。对于QObject类QMetaObject对象来说,这是一个NULL指针,因为 QObject没有父类。
class Animal : public QObject
{
Q_OBJECT
//.............
};
class Cat : public Animal
{
Q_OBJECT
//.............
}
那么,Cat::staticMetaObject.d.superdata 这个指针变量指向的对象是Animal::staticMetaObject,
Animal::staticMetaObject.d.superdata个指针变量指向的对象是QObject::staticMetaObject
QObject::staticMetaObject.d.superdat 这个指针变量的值为 NULL
但如果我们把上面class的定义修改为下面的定义,就不一样了:
class Animal : public QObject
{
// Q_OBJECT,这个 class 不定义这个
//.............
};
class Cat : public Animal
{ Q_OBJECT
//.............
}
那么,Cat::staticMetaObject.d.superdata 这个指针变量指向的对象是 QObject::staticMetaObject 因为 Animal::staticMetaObject 这个对象是不存在的
const char *stringdata:
顾名思义,这是一个指向string data的指针。但它和我们平时所使用的一般的字符串指针却很不一样,我们平时使用的字符串指针只是指向一个字符串的指针,而这个指针却指向的是很多个字符串。那么它不就是字符串数组吗?哈哈,也不是。因为C++的字符串数组要求数组中的每一个字符串拥有相同的长度,这样才能组成一个数组。那它是不是一个字符串指针数组呢?也不是,那它到底是什么呢?让我们来看一看它的具体值,还是让我们以QObject这个class的QMetaObject为例来说明 吧。
下面是QObject::staticMetaObject.d.stringdata指针所指向的多个字符串数组,其实它就是指向一个连续的内存区,而这个连续的内存区中保存了若干个字符串。
static const char qt_meta_stringdata_QObject[] =
{
"QObject\0\0destroyed(QObject*)\0destroyed()\0"
"deleteLater()\0_q_reregisterTimers(void*)\0"
"QString\0objectName\0parent\0QObject(QObject*)\0"
"QObject()\0"
};
既然他们都是不等长的字符串,那么Qt是如何来索引这些字符串,以便于在需要的时候能正确的找到他们呢?第三个成员正式登场了
const uint *data;
这个指针本质上就是指向一个正整数数组,只不过在不同的object中数组的长度都不尽相同,这取决于与之相对应的class中定义了多少 signal,slot,property。
这个整数数组的的值,有一部分指出了前一个变量(stringdata)中不同字符串的索引值,但是这里有一点需要注意的是,这里面的数值并不是直接标明了每一个字符串的索引值,这个数值还需要通过一个相应的算法计算之后,才能获得正确的字符串的索引值。
下面是QObject::staticMetaObject.d.data指针所指向的正整数数组的值。
static const uint qt_meta_data_QObject[] =
{
content:
2, // revision
0, // classname
0, 0, // classinfo
4, 12, // methods
1, 32, // properties
0, 0, // enums/sets
2, 35, // constructors //signals: signature, parameters, type, tag, flags
9, 8, 8, 8, 0x05,
29, 8, 8, 8, 0x25,
// slots: signature, parameters, type, tag, flags
41, 8, 8, 8, 0x0a,
55, 8, 8, 8, 0x08,
// properties: name, type, flags
90, 82, 0x0a095103,
// constructors: signature, parameters, type, tag, flags
108, 101, 8, 8, 0x0e,
126, 8, 8, 8, 0x2e,
0 // eod
简单的说明一下, 第一个section,就是 //content 区域的整数值,这一块区域在每一个QMetaObject的实体对象中数量都是相同的,含义也相同,但具体的值就不同了。专门有一个struct定义了这 个section,其含义在上面的注释中已经说的很清楚了。
struct QMetaObjectPrivate
{
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
int constructorCount, constructorData; //since revision 2
int flags; //since revision 3
int signalCount; //since revision 4
// revision 5 introduces changes in normalized signatures, no new members
// revision 6 added qt_static_metacall as a member of each Q_OBJECT and inside QMetaObject itself
};
第二个section,以 // signals 开头的这段。这个section中的数值指明了QObject这个class包含了两个signal,
第三个section,以 // slots 开头的这段。这个section中的数值指明了QObject这个class包含了两个slot。
第四个section,以 // properties 开头的这段。这个section中的数值指明了QObject这个class包含有一个属性定义。
第五个section,以 // constructors 开头的这段,指明了QObject这个class有两个constructor。
const void *extradata;
这是一个指向QMetaObjectExtraData数据结构的指针,关于这个指针,这里先略过。
对于每一个具体的整数值与其所指向的实体数据之间的对应算法,实在是有点儿麻烦,这里就不讲解细节了,有兴趣的朋友自己去
读一下源代码,一定会有很多发现。
七:connect, 幕后的故事
我们都知道,把一个signal和slot连接起来,需要使用QObject类的connect方法,它的作用就是把一个object的 signal和
另外一个object的slot连接起来,以达到对象间通讯的目的。 connect 在幕后到底都做了些什么事情?为什么emit一个signal后,
相应的slot都会被调用?好了,让我们来逐一解开其中的谜团.
SIGNAL 和 SLOT 宏定义
我们在调用connect方法的时候,一般都会这样写:
obj.connect(&obj, SIGNAL(destroyed()), &app, SLOT(aboutQt()));
我们看到,在这里signal和slot的名字都被包含在了两个大写的SIGNAL和SLOT中,这两个是什么呢?原来SIGNAL 和 SLOT
是Qt定义的两个宏。好了,让我们先来看看这两个宏都做了写什么事情,这里是这两个宏的定义:
# define SLOT(a) "1"#a #
#define SIGNAL(a) "2"#a
原来Qt把signal和slot都转化成了字符串,并且还在这个字符串的前面加上了附加的符号,signal前面加了’2’,slot前面加了’1’。 也就
是说,我们前面写了下面的connect调用,在经过编译器预处理之后,
就便成了: obj.connect(&obj, "2destroyed()", &app, "1aboutQt()”));
当connect函数被调用了之后,都会去检查这两个参数是否是使用这两个宏正确的转换而来的,它检查的根据就是这两个前置数
字,是否等于1或者是2,如果不是,connect函数当然就会失败啦!
然后,会去检查发送signal的对象是否有这个signal,方法就是查找这个对象的class所对应的staticMetaObject对象中所包含 的
d.stringdata所指向的字符串中是否包含这个signal的名字,在这个检查过程中,就会用到d.data所指向的那一串整数,通过这些整数
值来计算每一个具体字符串的起始地址。同理,还会使用同样的方法去检查slot,看响应这个signal的对象是否包含有相应的slot。
这两个检查的任何一个如果失败的话,connect函数就失败了,返回false.
前面的步骤都是在做一些必要的检查工作,下一步,就是要把发送signal的对象和响应signal的对象关联起来。