Qt 信号和槽机制

2,495 阅读22分钟

这篇文章篇幅很长,阅读可能需要10分钟以上,如果你是Qt的初学者,前面的6个章节就已经够用了,至少能够让你在一些普通的场面撑得起场子。 但如果你想了解的更深一点,最后一个章节是必不可少的内容。

Qt提供了很多我们学习的文档,甚至是源码。阅读源码在我们需要对它原生的控件做功能扩展的时候会显得尤为重要。

就像曾经有人问我,Qt在不同对象之间使用信号槽通信的,那么什么是信号槽,信号槽的本质知道吗?他们之间是怎么通信的,既然已经有了回调,为什么还要有信号槽?信号槽的第五个参数决定了他们的关联方式,队列关联和阻塞队列关联知道吗?阻塞是怎么阻塞的。能不能详细说说。

希望读完这篇文章,对你会有所帮助。

1、什么是信号槽?

Qt帮助文档里面这么说:

Signals and slots are used for communication between objects. The signals and slots mechanism is a central feature of Qt and probably the part that differs most from the features provided by other frameworks. Signals and slots are made possible by Qt’s meta-object system.

也就是说,信号和槽被用来两个对象之间的通讯。是Qt独有的核心机制。这也是区分于其他框架的特性,Qt的元对象系统使信号槽成为可能。信号和槽也让我们在Qt中有了回调技术的可替代方案。

上面提到了,Qt的元对象使得Qt的信号槽成为可能。那么元对象是什么。

大家都知道,Qt是一个利用标准C++编写的跨平台类库,对标准C++做了一些扩展。引入了一些类似信号槽、元对象、动态属性等特有的东西。元对象的引入是为了解决即使编译器不支持RTTI机制,也可以用过元对象系统(meta-object system )机制来获取运行时的类型信息

在Qt帮助文档中有这样的定义,meta-object依赖下面三件事:

  1. The QObject class provides a base class for objects that can take advantage of the meta-object system.(QObject类可以利用meta-object对象的类提供一个基类)
  2. The Q_OBJECT macro inside the private section of the class declaration is used to enable meta-object features, such as dynamic properties, signals, and slots.(在类的生命中加入Q_OBJECT宏可使用meta-object系统的功能,例如动态属性、信号、槽等)
  3. The Meta-Object Compiler (moc) supplies each QObject subclass with the necessary code to implement meta-object features.(元对象编译器(moc)为每个QObject子类提供元对象所需的代码)

总的理解一下就是,元对象可以做为QObject的一个基类,在类的声明中添加Q_OBJECT宏便可使用Qt独有的特性,比如动态属性、信号槽等。同时,moc编译器也实现了我们元对象系统的代码支持。

奇怪的是,我们已经有了C++的编译器,为什么Qt还要有自己的Meta-Object Compiler(moc)编译器呢?

这主要是因为,前面我们已经说了,Qt是对标准C++做了一些扩展,这些扩展对于C++的编译器来说并不认识呀。也就有了特定的Meta-Object Compiler(moc)编译器来进行一次转换,以便我们编写的代码能够被C++编译器正常编译。

如果我们需要编译一个Qt程序,他的步骤大致是这样的:

  1. 首先检查类中是否包含了Q_OBJECT宏,如果包含了,Meta-Object Compiler(moc)编译器会将该类首先进行处理,生成一个moc_xxx.cpp的文件。这个过程主要是为了对包含了Q_OBJECT宏中的一些关键字,比如:signals、slots、emit等转换为C++编译器可以识别的代码。
  2. 再进行一次C++编译器的编译。

我们后面会看到moc_xxx.cpp这个文件中的具体内容。

上面我们从Qt的信号槽机制,引伸到了meta-object system。我们是否知道了信号槽究竟是什么?

信号槽其实就是在Qt中对实例对象之间的通信做了一些替代。标准C++中我们一般使用回调做实例对象之间的通讯。但其实信号槽的本质也是回调。信号槽方便了我们在一对多实例对象之间的通信,免除了我们在这个过程中维护对象列表的过程。

2、信号槽的关联方式

从Qt帮助文档中找到了connect 函数的原型,我挑了其中的三个。

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
[static] QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection);
[static] QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor); //支持Lambda表达式

信号槽的关联在Qt4中我们一般写成下面这样:

connect(sender, SIGNAL(signal_xxx(int, const QString&)), reciever, SLOT(slot_xxx(int, const QString&)));

调用的是上面的第一个connect函数。

上面的这种方式存在一些问题,因为你在书写的时候一定要注意参数列表,数量、类型要保持一致才能正常关联。在Qt5之后,对这种关联方式做了一些优化。

connect(sender, &Object::signal_xxx, reciever, &Object::slot_xxx);

调用的是上面的第二个connect函数。

是不是方便了很多,至少我们不用再特意的关注信号和槽的参数列表要一一对应。但是这种方式会有一个艰难的选择,就是如果有多个同名但参数列表不一样的信号,如果我们直接这样写,编译器就会报错,需要指定具体是哪一个信息被关联。我们也有相应的解决方法:

connect(sender, static_cast<void(QObject::*)(const QString&)>(&Object::signal_xxx), reciever, &Object::slot_xxx);

举个例子:

QComboBoxcurrentIndexChanged 信号有两个。

void currentIndexChanged(int index);
void currentIndexChanged(const QString &text);

如果我们直接使用上面取地址的写法。编译器是会报错的。

connect(ui->cmb, &QComboBox::currentIndexChanged, this, &XXXWidget::slot_storageChanged);

为什么呢?

因为currentIndexChanged有两个不同参数列表的信号。编译的时候,编译器会无法确定需要哪个重载函数。所以我们需要告诉编译器需要调用哪个重载函数。

connect(ui->cmb, static_cast<void(QComboBox::*)(const QString&)>(&QComboBox::currentIndexChanged), this, &XXXWidget::slot_storageChanged);

这样我们就指定了这个关联需要调用哪个信号函数。

3、信号槽的使用

  1. 一个信号连接多个槽,每个槽都会被一一执行,但是如果在多线程情况下,执行的顺序是不确定的,因为主要依赖事件循环,并不一定能够确定多线程的顺序。
  2. 一个信号对应一个槽,这种方式是我们最常见的。
  3. 多个信号连接到一个槽,这种方式下,任何一个信号被 emit,槽函数都会被执行。
  4. 一个信号连接另一个信号,这种方式是被允许的,当信号被 emit后,相连的另一个信号会被emit
  5. 在Qt5之后,槽函数可以是 lambda 表达式。
  6. 信号可以通过引用的方式,返回出参。
  7. 信号可以继承,子类中可以直接 emit 父类的信号。
  8. 信号和槽既然能被 connect,同样也可以被 disconnect
disconnect(sender, 0, 0, 0);

最简单的取消关联的方法,在这行代码中,0 maybe any signal, any receiver, any slot。

4、信号槽的第五个参数,多线程情况下,信号槽在什么线程中执行,怎么控制?

或许你已经发现了,我们在上面看到了connect函数的最后一个函数Qt::ConnectionType type给了一个默认的值,Qt::AutoConnection。而这也是我们这个章节的主角。其实Qt connect函数的第五个参数表示了信号和槽的关联方式,目前是有4种方式。

Qt::AutoConnection :自动选择方式,也是默认的方式,这种方式主要看 槽函数是否在 和 emit 信号线程中,如果是,则Qt::DirectConnection方式被调用,否则,Qt::QueuedConnection方式被使用。

Qt::DirectConnection :直接关联,也就是槽函数在 emit 信号的线程中执行。这种方式和在 emit 信号的地方调用槽函数是同样的效果。

Qt::QueuedConnection : 这种方式在 emit 信号的时候,会调用 poseEvent函数给事件循环队列中发送一个事件,当Qt的事件循环调用接收方的事件循环式,槽函数被调用。槽函数在接收方所在的线程中被执行。

Qt::BlockingQueuedConnection :这种方式跟 Qt::QueuedConnection 是一样的,区别就是在槽函数执行返回之前,emit 信号的线程会被阻塞,所以这种方式不能用在 emit 信号 和 槽函数的执行在同一个线程里面的情况。因为会造成线程死锁。信号被 emit 后,线程被阻塞,等待槽函数指向线程的返回,但由于在同一线程中,并且现场被阻塞了,槽函数也就永远不会被执行。

Qt::UniqueConnection : 这是一个标志位,可以与上面的几种方式按位或。目的主要是为了防止重复关联。(当该标志位被设置时,如果重复关联相同的信号和槽,则会返回失败)。

实际使用情况下,我们还是要根据实际的情况选择不同的关联方式。

值得注意的是,如果是使用的队列关联方式,必须要使用Qt Meta-object system 中已知的参数类型,是因为使用队列的时候,Qt需要复制参数到事件循环中。如果我们不小心使用了自定义的参数类型,比如有个参数是自定义的枚举。编译器是会报错给我们的。

QObject::connect: Cannot queue arguments of type 'MyType'

当然这种错误我们也是有方法去解决的,我们使用注册机制将自定义的类型注册到Qt中。

qRegisterMetaType<LogType>("LogType");
connect(worker, &LoadWorker::signal_logMessage, this, &W3DimensionWidget::slot_logMessage);

看完了信号槽的第五个参数之后,我们先抛出一个问题,上面的第四种方式,阻塞队列方式,是怎么阻塞的?

5、信号槽的优缺点

我们都知道,信号槽在本质上其实是回调,那么为什么要多此一举在回调的基础上增加这一步呢?相比较回调,信号槽又有什么优越性呢?

回调函数的本质是你想让别人的代码执行你的代码,但别人的代码你又不能动。回调函数是函数指针的一种用法,如果多个类都关注某个类的状态变化,则需要维护一个列表,用来存放回调函数的地址。并且每个关注的类都要实现自己的代码来相应被关注类的状态变化。

相比较回调函数,信号槽主要体现了下面的两个优点:

  1. 松散耦合 : 信号槽降低了Qt对象之间的耦合度。Qt中 emit 信号的对象并不需要关心哪个对象的哪个槽会被执行。同样的,槽函数也并不关心是哪个信号被 emit了。他们各自就像课题分离一样各司其职。
  2. 类型安全 : 需要关联的信号和槽的签名必须是一致的。

信号和槽虽然增加了对象之间通信的灵活性,但就跟热力学第二定律一样,同时,也会损耗一些东西做为补偿。同回调函数相比,信号和槽的运行比回调慢10倍。原因如下:

  1. 信号和槽的关联,当信号被 emit 之后,Qt需要从队列里面遍历,找到需要执行的槽函数。这点在后面的源码分析中会有详细的介绍。
  2. 安全的遍历所有的关联,也就是当一个信号关联多个槽函数后,需要对所有的槽函数遍历并执行。
  3. 需要编解组传递的参数。
  4. 多线程的时候,槽函数的执行可能需要等待。多线程情况下,事件循环。

6、信号的屏蔽

虽然信号槽方便了我们很多的操作,但是信号槽不正当的使用也会给我们造成一些不必要的麻烦。比如,我们在某个类的对象中对QComboBox信号进行了关联。在类被使用过的过程中会频繁的对该QComboBoxitem进行修改。这也就意味着信号会被频繁的触发,槽函数会被频繁的调用。如果槽函数执行了比较耗时的操作,这也许会有意料之外的事情发生。

那么有没有什么方法能够让我们可以自由的控制信号被 emit,控制槽函数的执行呢?

Qt中已经提供给我们这种解决方法。blockSignals 函数会屏蔽掉信号和槽函数的执行。

bool QObject::blockSignals(bool block) Q_DECL_NOTHROW
{
    Q_D(QObject);
    bool previous = d->blockSig;
    d->blockSig = block;
    return previous;
}

这个函数返回上一次的状态,并设置新状态, Qt中也是通过这个状态去屏蔽的,但他并不能屏蔽掉信号被 emit,而是阻止了槽函数的执行。具体是怎么屏蔽的,我们后面在详细说。

举个例子:

ui->cmb->blockSignals(true);
ui->cmb->addItems(...);
ui->cmb->blockSignals(false);

7、信号槽的运行机制

我们从一个例子看起。

class MenuButton : public QPushButton
{
     Q_OBJECT
 public:
     using QPushButton::QPushButton;

 signals:
     void signal_btnClicked(const QString&, int, bool&);
     void signal_btnClicked(const QString&, int);
public slots:
	void slot_btnClicked(const QString&, int);
 private:
     void btnClick();
 };

上面的这个例子中,我们声明了一个信号 。

void signal_btnClicked(const QString&, int, bool&);

这个信号有三个参数,前面两个是比较常见的,最后一个通过引用的方式,其实跟我们日常调用函数是一样的,信号也可以用这样的方式来返回一个参数。

我们今天的目的不是去说明Qt 信号槽怎么定义和使用的。我们主要是想看看我们使用的幕后究竟发生了什么事情。比如,connect函数是怎么关联的,emit 信号之后到底是怎么执行槽函数的。

前面已经提到过,The Meta-Object Compiler (moc) 编译器会将C++的代码转换为C++编译器可以识别的标准C++源代码,并且以 moc_xxx.cpp为名。我们首先看下这个文件中有什么东西。这个文件里面有很多东西,比如Qt的一些动态属性,信号槽,或者类名等一些变量,其他的我们目前不关心,主要关注下信号槽方面的东西。由于代码篇幅过多,下面的代码对与信号槽无关的代码中做了一些精简。

void MenuButton::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<MenuButton *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->signal_btnClicked((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2])),(*reinterpret_cast< bool(*)>(_a[3]))); break;
        case 1: _t->signal_btnClicked((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2]))); break;
        case 2: _t->slot_btnClicked((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2]))); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (MenuButton::*)(const QString & , int , bool & );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MenuButton::signal_btnClicked)) {
                *result = 0;
                return;
            }
        }
        {
            using _t = void (MenuButton::*)(const QString & , int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MenuButton::signal_btnClicked)) {
                *result = 1;
                return;
            }
        }
    }
    //...
}

int MenuButton::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QPushButton::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 3)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 3;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 3)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 3;
    }

    //...

    return _id;
}

// SIGNAL 0
void MenuButton::signal_btnClicked(const QString & _t1, int _t2, bool & _t3)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)), const_cast<void*>(reinterpret_cast<const void*>(&_t3)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void MenuButton::signal_btnClicked(const QString & _t1, int _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

上面最后的这两个函数刚好对应我们在前面对信号的声明。并且按照我们在类定义中声明的信号的顺序进行了标注。这两个函数在什么时候运行呢?其实在emit 信号的时候,实质上也就是调用了这两个函数。

1、connect函数的实现

我们都知道,信号槽要起作用,首先要用QObject::connect 函数进行关联。并且因为Qt4 和Qt5调用的函数是不一样的,但两个函数的大致逻辑是一样的,在日常使用过程中,推荐使用Qt5的关联方式,所以下面我们主要针对Qt5的方式看一写其中的逻辑。

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)
{
    typedef QtPrivate::FunctionPointer<Func1> SignalType;
    typedef QtPrivate::FunctionPointer<Func2> SlotType;

    //...

    const int *types = nullptr;
    if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)
        types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();

    return connectImpl(sender, reinterpret_cast<void **>(&signal),
                    receiver, reinterpret_cast<void **>(&slot),
                    new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,
                                    typename SignalType::ReturnType>(slot),
                        type, types, &SignalType::Object::staticMetaObject);
}

从上面这个函数看,在调用了connect 函数之后,除了进行一些必要的防错判断被我用 //...代替了之外,还对关联方式是队列方式的type进行了判断。最后调用了connectImpl函数。

QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signal,
                                     const QObject *receiver, void **slot,
                                     QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
                                     const int *types, const QMetaObject *senderMetaObject)
{
    if (!signal) {
        qWarning("QObject::connect: invalid null parameter");
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

    int signal_index = -1;
    void *args[] = { &signal_index, signal };
    for (; senderMetaObject && signal_index < 0; senderMetaObject = senderMetaObject->superClass()) {
        senderMetaObject->static_metacall(QMetaObject::IndexOfMethod, 0, args);
        if (signal_index >= 0 && signal_index < QMetaObjectPrivate::get(senderMetaObject)->signalCount)
            break;
    }
    if (!senderMetaObject) {
        qWarning("QObject::connect: signal not found in %s", sender->metaObject()->className());
        slotObj->destroyIfLastRef();
        return QMetaObject::Connection(0);
    }
    signal_index += QMetaObjectPrivate::signalOffset(senderMetaObject);
    return QObjectPrivate::connectImpl(sender, signal_index, receiver, slot, slotObj, type, types, senderMetaObject);
}

QObject::connectImpl 函数就比较有意思了。他首先对入参进行的检查,紧接着用了一个循环查询了信号ID和参数列表。

 int signal_index = -1;
void *args[] = { &signal_index, signal };
for (; senderMetaObject && signal_index < 0; senderMetaObject = senderMetaObject->superClass()) {
    senderMetaObject->static_metacall(QMetaObject::IndexOfMethod, 0, args);
    if (signal_index >= 0 && signal_index < QMetaObjectPrivate::get(senderMetaObject)->signalCount)
        break;
}

循环体中调用的函数 static_metacall好像在哪见过,往前翻一翻,我们在说moc_xxx.cpp文件中的内容的时候好像见过。这个循环的目的主要是为了找到信号在该类中的index。然后计算得出信号在该类中的偏移量,从前面的moc_xxx.cpp文件中我们可以看出,信号这些,在该文件中其实是被写成了一些数字(偏移量)。

紧接着调用了QObjectPrivate::connectImpl函数。

QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int signal_index,
                                             const QObject *receiver, void **slot,
                                             QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
                                             const int *types, const QMetaObject *senderMetaObject)
{
    if (!sender || !receiver || !slotObj || !senderMetaObject) {
        const char *senderString = sender ? sender->metaObject()->className()
                                        : senderMetaObject ? senderMetaObject->className()	
                                        : "Unknown";
        const char *receiverString = receiver ? receiver->metaObject()->className()
                                            : "Unknown";
        qWarning("QObject::connect(%s, %s): invalid null parameter", senderString, receiverString);
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

    QObject *s = const_cast<QObject *>(sender);
    QObject *r = const_cast<QObject *>(receiver);

    QOrderedMutexLocker locker(signalSlotLock(sender),
                            signalSlotLock(receiver));

    if (type & Qt::UniqueConnection && slot) {
        QObjectConnectionListVector *connectionLists = QObjectPrivate::get(s)->connectionLists;
        if (connectionLists && connectionLists->count() > signal_index) {
            const QObjectPrivate::Connection *c2 =
                (*connectionLists)[signal_index].first;

            while (c2) {
                if (c2->receiver == receiver && c2->isSlotObject && c2->slotObj->compare(slot)) {
                    slotObj->destroyIfLastRef();
                    return QMetaObject::Connection();
                }
                c2 = c2->nextConnectionList;
            }
        }
        type = static_cast<Qt::ConnectionType>(type ^ Qt::UniqueConnection);
    }

    QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection);
    c->sender = s;
    c->signal_index = signal_index;
    c->receiver = r;
    c->slotObj = slotObj;
    c->connectionType = type;
    c->isSlotObject = true;
    if (types) {
        c->argumentTypes.store(types);
        c->ownArgumentTypes = false;
    }

    QObjectPrivate::get(s)->addConnection(signal_index, c.data());
    QMetaObject::Connection ret(c.take());
    locker.unlock();

    QMetaMethod method = QMetaObjectPrivate::signal(senderMetaObject, signal_index);
    Q_ASSERT(method.isValid());
    s->connectNotify(method);

    return ret;
}

上面的这个函数其实是很简单的,前面进行了参数有效性校验。中间有个if条件判断,主要是判断关联方式中是否有 Qt::UniqueConnection 标志位,如果有这个标志位,则遍历关联列表,判断是否已经存在了相同的关联,如果存在,则返回失败,否则将本次关联的 Qt::UniqueConnection 标志位设为有效。

后面构建了一个 QObjectPrivate::Connection 的对象,并将该对象放到关联列表中。

到此,connect关联信号和槽的部分就已经结束了。

2、emit 信号后,怎么调用槽函数

前面在看moc_xxx.cpp文件中的内容的时候,我们已经提到过,emit 信号后,实际上是调用了该文件中的对象的函数。

// SIGNAL 0
void MenuButton::signal_btnClicked(const QString & _t1, int _t2, bool & _t3)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)), const_cast<void*>(reinterpret_cast<const void*>(&_t3)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void MenuButton::signal_btnClicked(const QString & _t1, int _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

而上面的这两个函数,最终都调用了 QMetaObject::activate 这个函数,因此,搞明白这个函数是怎么运行的,也就成了我们搞明白槽函数是怎么执行的关键。

很多人可能在源码中找过这个函数,我相信很多人可能是没有找到的,说实话这个函数藏得是有点深。 这个函数其实是在源码的 qobject.cpp中的。同名函数有三个,最终都调用到了下面这个函数中。

/*!
    \internal
*/
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;

    if (sender->d_func()->blockSig)
        return;

    Q_TRACE_SCOPE(QMetaObject_activate, sender, signal_index);

    if (sender->d_func()->isDeclarativeSignalConnected(signal_index)
            && QAbstractDeclarativeData::signalEmitted) {
        Q_TRACE_SCOPE(QMetaObject_activate_declarative_signal, sender, signal_index);
        QAbstractDeclarativeData::signalEmitted(sender->d_func()->declarativeData, sender,
                                                signal_index, argv);
    }

    if (!sender->d_func()->isSignalConnected(signal_index, /*checkDeclarative =*/ false)
        && !qt_signal_spy_callback_set.signal_begin_callback
        && !qt_signal_spy_callback_set.signal_end_callback) {
        // The possible declarative connection is done, and nothing else is connected, so:
        return;
    }

    void *empty_argv[] = { 0 };
    if (qt_signal_spy_callback_set.signal_begin_callback != 0) {
        qt_signal_spy_callback_set.signal_begin_callback(sender, signal_index,
                                                        argv ? argv : empty_argv);
    }

    {
    QMutexLocker locker(signalSlotLock(sender));
    struct ConnectionListsRef {
        QObjectConnectionListVector *connectionLists;
        ConnectionListsRef(QObjectConnectionListVector *connectionLists) : connectionLists(connectionLists)
        {
            if (connectionLists)
                ++connectionLists->inUse;
        }
        ~ConnectionListsRef()
        {
            if (!connectionLists)
                return;

            --connectionLists->inUse;
            Q_ASSERT(connectionLists->inUse >= 0);
            if (connectionLists->orphaned) {
                if (!connectionLists->inUse)
                    delete connectionLists;
            }
        }

        QObjectConnectionListVector *operator->() const { return connectionLists; }
    };
    ConnectionListsRef connectionLists = sender->d_func()->connectionLists;
    if (!connectionLists.connectionLists) {
        locker.unlock();
        if (qt_signal_spy_callback_set.signal_end_callback != 0)
            qt_signal_spy_callback_set.signal_end_callback(sender, signal_index);
        return;
    }

    const QObjectPrivate::ConnectionList *list;
    if (signal_index < connectionLists->count())
        list = &connectionLists->at(signal_index);
    else
        list = &connectionLists->allsignals;

    Qt::HANDLE currentThreadId = QThread::currentThreadId();

    do {
        QObjectPrivate::Connection *c = list->first;
        if (!c) continue;
        // We need to check against last here to ensure that signals added
        // during the signal emission are not emitted in this emission.
        QObjectPrivate::Connection *last = list->last;

        do {
            if (!c->receiver)
                continue;

            QObject * const receiver = c->receiver;
            const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId.load();

            // determine if this connection should be sent immediately or
            // put into the event queue
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker);
                continue;
#if QT_CONFIG(thread)
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
                if (receiverInSameThread) {
                    qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
                    "Sender is %s(%p), receiver is %s(%p)",
                    sender->metaObject()->className(), sender,
                    receiver->metaObject()->className(), receiver);
                }
                QSemaphore semaphore;
                QMetaCallEvent *ev = c->isSlotObject ?
                    new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) :
                    new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore);
                QCoreApplication::postEvent(receiver, ev);
                locker.unlock();
                semaphore.acquire();
                locker.relock();
                continue;
#endif
            }

            QConnectionSenderSwitcher sw;

            if (receiverInSameThread) {
                sw.switchSender(receiver, sender, signal_index);
            }
            if (c->isSlotObject) {
                c->slotObj->ref();
                QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj);
                locker.unlock();

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.data());
                    obj->call(receiver, argv ? argv : empty_argv);
                }

                // Make sure the slot object gets destroyed before the mutex is locked again, as the
                // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool,
                // and that would deadlock if the pool happens to return the same mutex.
                obj.reset();

                locker.relock();
            } else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
                //we compare the vtable to make sure we are not in the destructor of the object.
                const int methodIndex = c->method();
                const int method_relative = c->method_relative;
                const auto callFunction = c->callFunction;
                locker.unlock();
                if (qt_signal_spy_callback_set.slot_begin_callback != 0)
                    qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv);

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
                    callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
                }

                if (qt_signal_spy_callback_set.slot_end_callback != 0)
                    qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex);
                locker.relock();
            } else {
                const int method = c->method_relative + c->method_offset;
                locker.unlock();

                if (qt_signal_spy_callback_set.slot_begin_callback != 0) {
                    qt_signal_spy_callback_set.slot_begin_callback(receiver,
                                                                method,
                                                                argv ? argv : empty_argv);
                }

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
                    metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
                }

                if (qt_signal_spy_callback_set.slot_end_callback != 0)
                    qt_signal_spy_callback_set.slot_end_callback(receiver, method);

                locker.relock();
            }

            if (connectionLists->orphaned)
                break;
        } while (c != last && (c = c->nextConnectionList) != 0);

        if (connectionLists->orphaned)
            break;
    } while (list != &connectionLists->allsignals &&
        //start over for all signals;
        ((list = &connectionLists->allsignals), true));

    }

    if (qt_signal_spy_callback_set.signal_end_callback != 0)
        qt_signal_spy_callback_set.signal_end_callback(sender, signal_index);
}

这个函数有180行,看着会比较吓人,其实如果你去掉一些参数校验或者其他一些无关痛痒的代码,剩下的也就没有多少了。就像在第一个 do{...}while()循环开始之前,前面的都是一些参数的校验和数据的准备,比如从类中拿到关联列表,比如获取获取当前线程ID等等。

上面这个函数有两个嵌套的 do{...}while()循环。第一个循环是在遍历关联列表,这个其实我们能已经在前面说过了,信号槽的缺点就是因为要遍历关联列表,所以他的效率比回调要慢10倍。

 do {
    QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // We need to check against last here to ensure that signals added
    // during the signal emission are not emitted in this emission.
    QObjectPrivate::Connection *last = list->last;

    //...
} while (list != &connectionLists->allsignals &&
    //start over for all signals;
    ((list = &connectionLists->allsignals), true));

第二个 do{...}while() 中才对每个关联进行了单独的处理。

 if (!c->receiver)
        continue;

QObject * const receiver = c->receiver;
const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId.load();

// determine if this connection should be sent immediately or
// put into the event queue
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
    || (c->connectionType == Qt::QueuedConnection)) {
    queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker);
    continue;
} 

首先判断关联的方式,如果是自动关联并且 receiver 和 sender 不在同一个线程或者关联方式是队列关联,则直接调用 queued_activate 函数进行处理,该函数中的处理部分暂时先不看了,最终目的是创建一个事件,并且使用 QCoreApplication::postEvent 方法将该事件post 给 receiver 的事件队列中去。queued_activate 函数的部分代码如下:

static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv, QMutexLocker &locker)
{
    //....
    
    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal, nargs, types, args) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs, types, args);
    QCoreApplication::postEvent(c->receiver, ev);
}

接下来,如果关联方式是 Qt::BlockingQueuedConnection 方式,首先会判断 receiver 和 sender 是不是在同一个线程。

如果是在同一个线程中,则直接报错线程死锁。

如果不在一个线程中,则创建事件并将该事件post到receiver的事件队列中。

跟上面的队列关联是差不多的,区别是这种方式会阻塞 sender 线程一直到 receiver 线程槽函数执行完成。

else if (c->connectionType == Qt::BlockingQueuedConnection) {
    if (receiverInSameThread) {
        qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
        "Sender is %s(%p), receiver is %s(%p)",
        sender->metaObject()->className(), sender,
        receiver->metaObject()->className(), receiver);
    }
    QSemaphore semaphore;
    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore);
    QCoreApplication::postEvent(receiver, ev);
    locker.unlock();
    semaphore.acquire();
    locker.relock();
    continue;
}

我们从代码中可以看到,创建了一个 QSemaphore 的对象 semaphore,在 QCoreApplication::postEvent 函数调用之后,调用了 semaphore.acquire(); 函数。这样做的目的是什么? 其实 QSemaphore 是Qt中的信号量,功能类似于 C++中 mutex。

这样也就实现了线程的同步问题。同时也回答了我们在前面第四部分提出的问题。

接下来的代码,我们说完了自动关联和队列关联的方式,也就剩下了直接关联的方式,即 sender 和 receiver 在同一个线程中,过程也无非就是判断槽函数的类型,并且根据不同的类型选择合适的调用方式调用。

如果槽函数是槽函数对象,则找到槽函数的对象,并且调用他的 call 函数。

 if (c->isSlotObject) {
    c->slotObj->ref();
    QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj);
    locker.unlock();

    {
        Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.data());
        obj->call(receiver, argv ? argv : empty_argv);
    }

    // Make sure the slot object gets destroyed before the mutex is locked again, as the
    // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool,
    // and that would deadlock if the pool happens to return the same mutex.
    obj.reset();

    locker.relock();
} 

如果是回调函数,并且满足方法的偏移在 接收者的方法偏移之内。则直接调用其回调函数。

else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
   //we compare the vtable to make sure we are not in the destructor of the object.
    const int methodIndex = c->method();
    const int method_relative = c->method_relative;
    const auto callFunction = c->callFunction;
    locker.unlock();
    if (qt_signal_spy_callback_set.slot_begin_callback != 0)
        qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv);

    {
        Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
        callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
    }

    if (qt_signal_spy_callback_set.slot_end_callback != 0)
        qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex);
    locker.relock();
}
else {
   const int method = c->method_relative + c->method_offset;
    locker.unlock();

    if (qt_signal_spy_callback_set.slot_begin_callback != 0) {
        qt_signal_spy_callback_set.slot_begin_callback(receiver,
                                                    method,
                                                    argv ? argv : empty_argv);
    }

    {
        Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
        metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
    }

    if (qt_signal_spy_callback_set.slot_end_callback != 0)
        qt_signal_spy_callback_set.slot_end_callback(receiver, method);

    locker.relock();
}

虽然这些操作有不一样的调用方法,最终还是会调到 moc_xxx.cpp文件中的 int Object::qt_metacall(QMetaObject::Call _c, int _id, void **_a) 函数。

3、对象信号的屏蔽

前面我们已经看了信号的屏蔽,并且知道了屏蔽的其实是槽函数的执行,我们从上面的activate 函数中也能找到答案。在该函数的前几行有个 if 条件的判断。如果读取sender对象的 blockSig 状态为true。则直接返回,退出activate函数的执行。

if (sender->d_func()->blockSig)
    return;

中间可能有一些理解不对的地方,欢迎指出交流。