深入分析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个宏
SIGNALSLOT,里面包裹着对应的函数和参数类型列表(不能出现参数名)。这种写法基于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信号。

posted @ 2025-04-27 15:21  thammer  阅读(128)  评论(0)    收藏  举报