深入分析Qt信号-槽系列(一)
Qt的信号-槽是它的一个核心机制,入门Qt两年了,对于这一块总感觉一知半解。深入分析下这套机制,来破除开发过程中的疑惑。
1. 信号和槽的连接写法
老写法:
在Qt5之前,connect只有3个原型:
static QMetaObject::Connection connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &method,
Qt::ConnectionType type = Qt::AutoConnection);
inline QMetaObject::Connection connect(const QObject *sender, const char *signal,
const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
第三个只不过是第一个的一个wrapper。而第二个只是把第一个内部的一些行为交给调用者处理,所谓的内部行为是什么?就是QMetaObject系统根据字符串查找QMetaMethod的过程。
使用方法示例:
//传递字符串,由connect内部根据字符串,基于元对象系统,获取信号、槽各自的QMetaMethod,实现信号-槽之间的关联
QObject::connect(senderObject, SIGNAL(mySignal(int)), SLOT(onMySignalReceived(int)));
QObject::connect(senderObject, SIGNAL(mySignal(int)), receiverObject, SLOT(onMySignalReceived(int)));
QObject::connect(senderObject, SIGNAL(mySignal(int)), receiverObject, SLOT(onMySignalReceived(int)), connectType);
//传递QMetaMethod,把上面根据字符串查找QMetaMethod的过程拿出来了
QObject::connect(senderObject, metaSignal, receiverObject, metaSlot);
QObject::connect(senderObject, metaSignal, receiverObject, metaSlot, connectType);
它传入若干个参数,依次是信号发送对象指针、信号发送函数、信号接收对象指针、信号接收对象的槽函数、连接类型
,其中第一种写法只是第二种写法的一个wrapper,默认会把this指针传给第二种写法,而第二种写法只是第三种写法使用默认参数的形式,最后一个参数默认为
Qt::AutoConnection
。它这里使用了2个宏
SIGNAL
和SLOT
,里面包裹着对应的函数和参数类型列表(不能出现参数名)。这种写法基于Qt的元对象机制,并且要求信号和槽都是QObject或其子类的成员函数(静态或非静态都可以)。
SIGNAL和SLOT实际是两个拼接字符串,然后把字符串传递给qFlagLocation函数,而qFlagLocation函数又会原样返回这个字符串的宏:
#define QT_STRINGIFY2(x) #x
#define QT_STRINGIFY(x) QT_STRINGIFY2(x)
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
# define QLOCATION "\0" __FILE__ ":" QT_STRINGIFY(__LINE__)
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
//把字符串存储到线程私有数据里面
const char *qFlagLocation(const char *method)
{
QThreadData *currentThreadData = QThreadData::current(false);
if (currentThreadData != nullptr)
currentThreadData->flaggedSignatures.store(method);
return method;
}
以SIGNAL(mySignal(int))为例,手动展开这个宏,我分析C++复杂的模块和宏时,一般会用到cppinsights
这个网站来帮我快速验证,还有它的兄弟网站Compiler Explorer。屏蔽线程私有数据的操作相关代码,经过工具网站的展开:
__attribute__((visibility("default"))) const char * qFlagLocation(const char * method);
__attribute__((visibility("default"))) const char * qFlagLocation(const char * method)
{
return method;
}
int main()
{
//注意这里展开过程中__FUNCTION__和__LINE__宏被展开为cppinsight这个网站环境中的了,实际是你自己编译环境中的。
qFlagLocation("2mySignal(int)\000/home/insights/insights.cpp:19");
return 0;
}
这个拼接的字符串被一个字符串结束符\0
分隔为两个部分,前面的是给元对象系统用的,用于实现信号槽调用,后面这一段是报错时提示用的,看一个例子:
#include <QApplication>
#include <QObject>
#include <QDebug>
class SenderObject : public QObject
{
Q_OBJECT
public:
explicit SenderObject(QObject *parent = nullptr)
: QObject(parent)
{
qDebug() << "SenderObject";
}
~SenderObject() override
{
qDebug() << "~SenderObject";
}
Q_SIGNALS:
void signalA(int arg);
};
class ReceiverObject : public QObject
{
Q_OBJECT
public:
explicit ReceiverObject(QObject *parent = nullptr)
: QObject(parent)
{
qDebug() << "ReceiverObject";
}
~ReceiverObject() override
{
qDebug() << "~ReceiverObject";
}
public Q_SLOTS:
void handleSignalA(int arg)
{
qDebug() << "handleSignalA, arg:" << arg;
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
SenderObject *sender = new SenderObject();
ReceiverObject *receiver = new ReceiverObject();
//这里连接了一个完全不存在的信号notExsitSignal
QObject::connect(sender, SIGNAL(notExsitSignal(int)), receiver, SLOT(handleSignalA(int)));
emit sender->signalA(1);
delete sender;
delete receiver;
return QApplication::exec();
}
#include "main.moc"
编译没有任何问题,而实际运行时,输出:
SenderObject
ReceiverObject
QObject::connect: No such signal SenderObject::notExsitSignal(int) in /home/thammer/test/qtAnalysis/main.cpp:189
~SenderObject
~ReceiverObject
后面这个文件,行号就是SIGNAL宏拼接字符串的后半部分,设置到线程私有数据后,从这里面取到的。
这也暴露了老写法的缺陷,编译时没做检查,要运行时才能发现问题。不过在Qt5引入新写法后,这个问题得到的解决。自Qt5后不再建议用老写法,除非不得已。后续话题也不再讨论这块。自此发现老写法有如下缺陷:
- 信号接收者的局限性
- 信号发送者和接收者必须都是QObject对象或者其派生类的方法,其他类型callable都无法使用,比如普通函数,lambda,functor,另一个信号。
- 编译期防御机制缺失
- 信号-槽连接不做编译检查,依赖运行时检查。
- 运行时性能瓶颈
- 元对象信号槽调用流程:字符串哈希 → QMetaObject查询 → 参数类型匹配 → 动态调用,相比于Qt5的新写法,效率低下。
新写法:
Qt5的connect原型新引入了5个重载函数模版,加上之前的3个重载成员方法,一共8个。
//兼容老式写法
static QMetaObject::Connection connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &method,
Qt::ConnectionType type = Qt::AutoConnection);
inline QMetaObject::Connection connect(const QObject *sender, const char *signal,
const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
//新引入的函数模版
//Connect a signal to a pointer to qobject member function(把信号和QObject的成员函数指针连接)
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
//connect to a function pointer (not a member)(把信号和函数指针(非成员函数)连接)
template <typename Func1, typename Func2>
static inline typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot)
//connect to a function pointer (not a member)(把信号和函数指针(非成员函数)连接)
template <typename Func1, typename Func2>
static inline typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0 &&
!QtPrivate::FunctionPointer<Func2>::IsPointerToMemberFunction, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
//connect to a functor(把信号和仿函数连接)
template <typename Func1, typename Func2>
static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot)
//connect to a functor, with a "context" object defining in which event loop is going to be executed
//(把信号和仿函数连接,带一个'context'对象,定义槽将在哪个事件循环中执行)
template <typename Func1, typename Func2>
static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
可以看到新增的函数模版,使得除了支持QObject及其派生类的成员函数,还支持了普通的函数指针和仿函数,实际呢lambda的实现好像就是匿名类的仿函数。
下面的例子基于上面例子,新增一个普通函数和一个带仿函数的类(对操作符()进行了重载的类)
void nonMemberFunction(int arg)
{
qDebug() << "nonMemberFunction,arg:" << arg;
}
class Functor
{
public:
void operator()(int arg) const
{
qDebug() << "Functor, arg:" << arg;
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
SenderObject *sender = new SenderObject();
ReceiverObject *receiver = new ReceiverObject();
int signalIndex = sender->metaObject()->indexOfSignal("signalA(int)");
int slotIndex = receiver->metaObject()->indexOfSlot("handleSignalA(int)");
QMetaMethod signal = sender->metaObject()->method(signalIndex);
QMetaMethod slot = receiver->metaObject()->method(slotIndex);
//QMetaMethod方式
QObject::connect(sender, signal, receiver, slot);
//字符串方式
QObject::connect(sender, SIGNAL(signalA(int)), receiver, SLOT(handleSignalA(int)));
//QObject及其派生类的成员函数指针
QObject::connect(sender, &SenderObject::signalA, receiver, &ReceiverObject::handleSignalA);
//普通函数指针
QObject::connect(sender, &SenderObject::signalA, nonMemberFunction);
//带一个QObject及其派生类的receiver对象指针
QObject::connect(sender, &SenderObject::signalA, receiver, nonMemberFunction);
//lambda(即仿函数)
QObject::connect(sender, &SenderObject::signalA, [](int arg) {
qDebug() << "lambda, arg:" << arg;
});
//仿函数
QObject::connect(sender, &SenderObject::signalA, Functor());
//带一个QObject及其派生类的receiver对象指针
QObject::connect(sender, &SenderObject::signalA, receiver, Functor());
emit sender->signalA(1);
delete sender;
delete receiver;
return QApplication::exec();
}
#include "main.moc"
2. 信号和槽断开连接
以下面这个为例:
//QObject及其派生类的成员函数指针
QObject::connect(sender, &SenderObject::signalA, receiver, &ReceiverObject::handleSignalA);
与之对应的disconnect写法有如下几种方式:
//精准匹配
/*---------断开-----发送者的--------信号-------------之前已连接到接收者的---------槽-------------------*/
QObject::disconnect(sender, &SenderObject::signalA, receiver, &ReceiverObject::handleSignalA);
//通配,任意一个部分如果是nullptr表示通配,在语境中加一个“任何”即可
/*---------断开-----发送者的--------信号-------------之前已连接到接收者的---------任何槽-------------*/
QObject::disconnect(sender, &SenderObject::signalA, receiver, nullptr);
/*---------断开-----发送者的--------信号-------------之前已连接到任何接收者的------任何槽------------*/
QObject::disconnect(sender, &SenderObject::signalA, nullptr, nullptr);
/*---------断开-----发送者的----任何信号-------------之前已连接到接收者的----------任何槽------------*/
QObject::disconnect(sender, nullptr, receiver, nullptr);
/*---------断开-----发送者的----任何信号-------------之前已连接到任何接收者的------任何槽------------*/
QObject::disconnect(sender, nullptr, nullptr, nullptr);
需要注意的是,信号的发送者不能为nullptr,接收者为nullptr时,槽也必须为nullptr。源码里面:
if (sender == nullptr || (receiver == nullptr && slot != nullptr)) {
qWarning("QObject::disconnect: Unexpected nullptr parameter");
return false;
}
当QObject对象及其派生对象析构时,会自动断开所有与它关联的信号-槽连接。这一切是在QObject的析构函数中实现的。
断开连接后,触发信号,不再会执行原来绑定的槽函数。这对于有些时候需要动态处理信号-槽连接的场景非常有效。
3. 阻塞信号
上一节说到信号-槽可以通过conncet,disconnect进行操作,实现信号-槽的断开和连接,实际上QObject类还有另一套机制实现信号-槽的阻断和接通,就是它的
QObject::blockSignals
方法:
bool QObject::blockSignals(bool block) noexcept
{
Q_D(QObject);
bool previous = d->blockSig;
d->blockSig = block;
return previous;
}
不过blockSignals是从sender方的视角来处理的。它只能控制sender是否可以发出信号。QObject对象默认不阻塞信号的发送。返回值表示设置前,上一次的阻塞状态。信号的阻塞操作不如connect/disconnect那样灵活,精细。为此Qt还专门定义了一个基于RAII的类
QSignalBlocker
。
需要注意的是,QObject类及其派生类在析构时,会把blockSig置为0,使之不再阻塞信号,它主要是为了让对象发出destroyed信号。