QT框架里面最大的特色就是在C++的基础上增加了元对象系统(Meta-Object System),而元对象系统里面最重要的内容就是信号与槽机制,这个机制是在C++语法的基础上实现的,使用了函数、函数指针、回调函数等概念。当然与我们自己去写函数所不同的是槽与信号机制会自动帮我们生成部分代码,比如我们写的信号函数就不需要写它的实现部分,这是因为在我们编译程序的时候,编译器会自动生成这一部分代码,当我们调用connect函数的时候,系统会自动将信号函数与槽函数相连接,于是当我们调用信号函数的时候,系统就会自动回调槽函数,不管你是在同一线程下调用或者在不同线程下调用,系统都会自动评估,并在合理的时候触发函数,以此来保证线程的安全。信号与槽机制是线程安全的,这可以使得我们在调用的时候不用再额外的增加过多保证线程同步的代码,为了实现元对象系统,QT把所有相关实现写在了QObject类中,所以当你想使用元对象系统的时候,你所写的类需要继承自QObject,包括QT自带的所有类都是继承自QObject,所以分析QObject的代码,对了解QT的元对象机制有非常大的帮助,我并不打算把QObject类的每一行代码都写下来,只想把其中比较关键的内容或者对分析QT源码有帮助的内容介绍一下。
安装qt之后(我使用的是online自动安装),安装目录下有\5.10.1\Src\qtbase\src目录, 这里的所有文件夹名都对应着 Qt 的模块的名字 :gui,network等。
我们从最核心的 QtCore 开始,这个模块对应的是corelib文件夹。之所以选择 QObject,一是因为它是 Qt 的核心类,另外一个很重要的原因是,QObject类是一个典型的Qt类,我们可以通过这个类学习到Qt的设计思路。
编写 Qt 代码的时候,使用的语句是 #include<QObject>, 对应的是QObject文件,该文件的位置可以在qt creator中用ctrl+鼠标查看,具体目录为\5.10.1\mingw53_32\include\QtCore,该文件中只有一句话:#include "qobject.h",qobject .h的位置为 \5.10.1\Src\qtbase\src\corelib\kernel,在该kernel文件夹下,可以找到有四个文件以 qobject 打头:
qobject.h:QObject 的类定义,这个就是QObject 文件引用的文件,也就是我们使用的实际头文件;
qobject.cpp:QObject的实现代码;
qobjectdefs.h:这个文件中定义了很多用到的宏,并且定义了QMetaObject类,而这个类是实现signal-slot的基础;
qobject_p.h:对 QObject的辅助数据类;
还会看到另外两个文件:qobjectcleanuphandler.h 和qobjectcleanuphandler.cpp。不过如果打开这两个文件就会发现,这里面定义的是一个QObjectCleanupHandler类,而这个类是继承了 QObject 的,因此这只是一个普通的工具类,不在我们目前的讨论之列。因此我们可以认为,QOjbect类是由4个文件共同实现的:qobject.h,qobject.cpp,qobjectdefs.h和qobject_p.h。
1.宏Q_OBJECT(qobjectdefs.h文件中)
这个宏展开以后是如下定义:
#define Q_OBJECT \
public: \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
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: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")
知识兔你可以看到这个宏定义了一些函数,并且函数名都带有meta,所以不难猜到这些函数和QT的元对象系统是有关系的,实际上你在qobject.cpp里面是找不到这些函数的实现的,它们的实现都在moc_qobject.cpp里面。QT的元对象系统是这样处理的,当你编译你的工程时,它会去遍历所有C++文件,当发现某一个类的私有部分有声明Q_OBJECT这个宏时,就会自动生成一个moc_*.cpp的文件,这个文件会生成信号的实现函数,Q_OBJECT宏里面定义的那些函数也会在这个文件里面实现,并生成与类相关的元对象。这就是为什么我们定义一个信号的时候,不需要实现它,因为它的实现已经写在moc_*.cpp文件里面了。
2.Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)
这个宏是定义一个属性,属性也是元对象系统的内容之一,实际上我们在做界面设计的时候经常会用到属性,比如修改Label的显示内容,需要用到Text属性,修改窗体长宽等等,在你做界面设计的时候,属性编辑框里面所显示的就是当前对象的所有属性,而这些属性的定义就是用上面的宏来定义的。实际上属性和变量是有点相似的,都是读值和写值的功能,那为什么不直接对变量操作就好了?虽然看起来相似,但是还是有不同点,第一属性可以定义为可读写的,也可以定义为只读的,比如有些数据我们只在类的内部做修改不允许在外部做修改,但是有时候又需要在外部查看这个值,就可以设置为只读属性,而变量是做不到这点的,你把变量放在public部分,那么这个变量就可以在任何地方被修改,这就破坏了类的封装性。第二属性可以定义信号,当属性变化的时候触发信号,这样我们可以在信号触发时做一些工作,比如当你设置LineEdit为readonly时,你会发现输入框的背景颜色被改变了,这就可以通过属性变化的信号来处理。
3.Q_DECLARE_PRIVATE(QObject)
这个宏的定义如下:
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \
friend class Class##Private;
知识兔这个宏首先创建了两个内联函数,返回值都是QObjectPrivate *,并且声明QObjectPrivate 为友元类,QObjectPrivate这个类是在qobject_p.h中定义,它继承至QObjectData,你可以看到d_func()是将d_prt强制转换为QObjectPrivate *类型,而d_prt这个指针在QObject里面定义的是QObjectData的指针类型,所以这里可以进行强转,QObjectPrivate这个类主要存放QOject类需要用到的一些子对象,变量等。为什么要介绍这个宏,如果你有看QT源码习惯的话,你会发现几乎每一个类都用到了这个宏,我们自己写的类会经常把类内部用的变量声明在private部分,但是QT源码并不是这样做的,它的做法是给每个类创建一个以类名+Private的类,例如QObject对应的就是QObjectPrivate,这个类实际上就是用来存放QObject需要用到的所有私有变量和私有对象,而QObject更多的是函数实现,你去看其他的源码也是如此,子对象声明在Q*Private中,而本类只实现函数。
4.构造函数
QObject::QObject(QObject *parent)
: d_ptr(new QObjectPrivate)
{
Q_D(QObject);
d_ptr->q_ptr = this;
d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current();
d->threadData->ref();
if (parent) {
QT_TRY {
if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))
parent = 0;
setParent(parent);
} QT_CATCH(...) {
d->threadData->deref();
QT_RETHROW;
}
}
#if QT_VERSION < 0x60000
qt_addObject(this);
#endif
if (Q_UNLIKELY(qtHookData[QHooks::AddQObject]))
reinterpret_cast<QHooks::AddQObjectCallback>(qtHookData[QHooks::AddQObject])(this);
}
知识兔(1)首先第一步就创建d_ptr指针。
(2)Q_D(QObject);这个宏你可以在QT的很多源码里面看到。它展开以后是下面的样子:#define Q_D(Class) Class##Private * const d = d_func();
d_fun()函数前面讲到了,其实就是返回d_ptr了。所以这个宏的意思是定义一个指针d指向d_ptr;
(3)d_ptr->q_ptr = this;
q_ptr是QOject类型,这里把this指针赋给了它,所以使得QObjectPrivate可以回调QOject的函数。
(4)初始化threadData
5.moveToThread
(1)如果要移动的线程和Object本身就是同一线程,那么直接返回
Q_D(QObject);
if (d->threadData->thread == targetThread) {
// object is already in this thread
return;
}
知识兔(2)如果parent不为空,不允许移动到其他线程,子类必需与父类在同一线程。
if (d->parent != 0) {
qWarning("QObject::moveToThread: Cannot move objects with a parent");
return;
}
知识兔(3)如果对象是窗体类,不允许移动到线程,窗体类必需在主线程运行,在子线程去直接调用窗体控件都是不安全的,可能导致程序崩溃,合理的做法是通过信号槽机制。
if (d->isWidget) {
qWarning("QObject::moveToThread: Widgets cannot be moved to a new thread");
return;
}
知识兔(4)只有在对象所在线程才能将对象移动到另一个线程,不能在其他线程将对象移动到某个线程,这种操作是不被允许的。
QThreadData *currentData = QThreadData::current();
QThreadData *targetData = targetThread ? QThreadData::get2(targetThread) : Q_NULLPTR;
if (d->threadData->thread == 0 && currentData == targetData) {
// one exception to the rule: we allow moving objects with no thread affinity to the current thread
currentData = d->threadData;
} else if (d->threadData != currentData) {
qWarning("QObject::moveToThread: Current thread (%p) is not the object's thread (%p).\n"
"Cannot move to target thread (%p)\n",
currentData->thread.load(), d->threadData->thread.load(), targetData ? targetData->thread.load() : Q_NULLPTR);
#ifdef Q_OS_MAC
qWarning("You might be loading two sets of Qt binaries into the same process. "
"Check that all plugins are compiled against the right Qt binaries. Export "
"DYLD_PRINT_LIBRARIES=1 and check that only one set of binaries are being loaded.");
#endif
return;
}
知识兔(5)线程转移
//为转移线程准备,遍历所有子对象,并给每一个子对象发送一个QEvent::ThreadChange的事件。
d->moveToThread_helper();
if (!targetData)
targetData = new QThreadData(0);
//为转移事件上锁
QOrderedMutexLocker locker(¤tData->postEventList.mutex,
&targetData->postEventList.mutex);
currentData->ref();
//遍历所有子对象及自身,将currentData的postEventList里面的对象转移到targetData,将所有子对象及自身的threadData设置为targetData
d_func()->setThreadData_helper(currentData, targetData);
locker.unlock();
currentData->deref();
知识兔6.connect函数
connet的重构函数很多,这里选择其中一个来分析。
QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &method,
Qt::ConnectionType type)
知识兔(1)首选sender,receiver不能为空,signal必须是Signal类型,也就是声明在signals:下面,method不能为构造函数,不满足这几个条件则返回。
if (sender == 0
|| receiver == 0
|| signal.methodType() != QMetaMethod::Signal
|| method.methodType() == QMetaMethod::Constructor) {
qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
sender ? sender->metaObject()->className() : "(null)",
signal.methodSignature().constData(),
receiver ? receiver->metaObject()->className() : "(null)",
method.methodSignature().constData() );
return QMetaObject::Connection(0);
}
知识兔(2)检查signal和method是否真实存在,在编译期即使传入的信号不存在也不会报错,在运行期会检查是否存在,所以在写connect函数的时候要仔细检查,尽量使用&ClassName::functionName的方式让系统自动补全,当然也可以通过connect的返回值来判断调用是否成功,如调用不成功则抛出异常。
int signal_index;
int method_index;
{
int dummy;
QMetaObjectPrivate::memberIndexes(sender, signal, &signal_index, &dummy);
QMetaObjectPrivate::memberIndexes(receiver, method, &dummy, &method_index);
}
const QMetaObject *smeta = sender->metaObject();
const QMetaObject *rmeta = receiver->metaObject();
if (signal_index == -1) {
qWarning("QObject::connect: Can't find signal %s on instance of class %s",
signal.methodSignature().constData(), smeta->className());
return QMetaObject::Connection(0);
}
if (method_index == -1) {
qWarning("QObject::connect: Can't find method %s on instance of class %s",
method.methodSignature().constData(), rmeta->className());
return QMetaObject::Connection(0);
}
知识兔(3)检查signal和method的参数个数和类型是否是一致的,不一致则返回。
if (!QMetaObject::checkConnectArgs(signal.methodSignature().constData(), method.methodSignature().constData())) {
qWarning("QObject::connect: Incompatible sender/receiver arguments"
"\n %s::%s --> %s::%s",
smeta->className(), signal.methodSignature().constData(),
rmeta->className(), method.methodSignature().constData());
return QMetaObject::Connection(0);
}
知识兔(4)如果你设置的连接方式为QueuedConnection,那么所有的参数都必须是元数据类型,自定义的类型,如自定义的结构体或枚举必须注册为元数据类型,否则无法作为信号和槽的参数,因为最终要将这些参数加入到消息队列里面。
int *types = 0;
if ((type == Qt::QueuedConnection)
&& !(types = queuedConnectionTypes(signal.parameterTypes())))
return QMetaObject::Connection(0);
知识兔(5)所有的检查完毕,调用QMetaObject的Connection函数,而QMetaObject的Connection会创建一个Connection的对象,这个对象会保存信号和槽的函数对象,然后会把这个Connection对象保存到sender的一个数组中,当你触发信号的时候,因为Connection对象保存在了sender中,所以可以找到原来绑定的槽函数,然后回调槽函数。
QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
sender, signal_index, signal.enclosingMetaObject(), receiver, method_index, 0, type, types));
return handle;
知识兔