(二):Qt信号槽连接及触发原理

经过《Qt信号槽之—准备阶段》学习,我们知道信号是函数,里面调用了 QMetaObject::activate() 函数;
而对于接收端的槽函数或信号,是通过私有静态函数 qt_static_metacall() 调用元方法。
源头是有的,接收端也是有的,中间少了个桥,这个桥就是 Qt 元对象系统的技术内幕,我们需要阅读 Qt 核心源码才能知道。 后面三个小节就是把从源头到接收端的桥解析一下。

connect做了什么?

connect我们都知道是将信号和槽进行关联,具体这个桥梁如何搭建的,咱们下面进行分解。这里我们讲解Qt旧语法的信号槽,如下是一种咱们最常见的结构:

1.1 SIGNAL和SLOT宏

QMetaObject::Connection connect(const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection)

实例:

bool flg = connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slot_set_label_number()));

我们可以看到实例信号,和槽分别用两个宏进行处理,下面来看一下SIGNAL和SLOT宏分别代表什么:

# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)

把信号作为字符串前面加上了“2”,对于槽函数,前面加"1",因此,上面的实例等同下面这句

bool flg = connect(ui.pushButton, "2clicked()", this, "1slot_set_label_number()");

1.2 connect函数详解

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
                                     const QObject *receiver, const char *method,
                                     Qt::ConnectionType type)
{
    if (sender == 0 || receiver == 0 || signal == 0 || method == 0) {
        qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
                 sender ? sender->metaObject()->className() : "(null)",
                 (signal && *signal) ? signal+1 : "(null)",
                 receiver ? receiver->metaObject()->className() : "(null)",
                 (method && *method) ? method+1 : "(null)");
        return QMetaObject::Connection(0);
    }
    
    QByteArray tmp_signal_name;
	//用来检测是否使用SINGAL关键字,如果没有则报错,返回一个空Connection
    if (!check_signal_macro(sender, signal, "connect", "bind"))
        return QMetaObject::Connection(0);
    
    const QMetaObject *smeta = sender->metaObject();
    const char *signal_arg = signal;
    ++signal; //skip code  去掉前面的数字
    QArgumentTypeArray signalTypes;
    Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7);
    QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
    //计算相对序号
    int signal_index = QMetaObjectPrivate::indexOfSignalRelative(
            &smeta, signalName, signalTypes.size(), signalTypes.constData());
    if (signal_index < 0) {
        //相对序号为负数,则表示没找到,则规范信号名(去除空格)
        // check for normalized signatures
        tmp_signal_name = QMetaObject::normalizedSignature(signal - 1);
        signal = tmp_signal_name.constData() + 1;

        signalTypes.clear();
        signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes);
        smeta = sender->metaObject();
       
        //再次查找序号
        signal_index = QMetaObjectPrivate::indexOfSignalRelative(
                &smeta, signalName, signalTypes.size(), signalTypes.constData());
    }
    if (signal_index < 0) {
        //两次都没找到,打印报警
        err_method_notfound(sender, signal_arg, "connect");
        err_info_about_objects("connect", sender, receiver);
        return QMetaObject::Connection(0);
    }
    //判断是否为克隆信号,如果是,则用原始序号
    signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
    //绝对序号
    signal_index += QMetaObjectPrivate::signalOffset(smeta);

    QByteArray tmp_method_name;
    int membcode = extract_code(method);

    if (!check_method_code(membcode, receiver, method, "connect"))
        return QMetaObject::Connection(0);
    const char *method_arg = method;
    ++method; // skip code

    QArgumentTypeArray methodTypes;
    QByteArray methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes);
    const QMetaObject *rmeta = receiver->metaObject();
    int method_index_relative = -1;
    Q_ASSERT(QMetaObjectPrivate::get(rmeta)->revision >= 7);
    
    switch (membcode) {
    case QSLOT_CODE:
             //如果 method 是槽函数类型,使用 QMetaObjectPrivate::indexOfSlotRelative 函数计算槽函数相对序号 method_index_relative;
        method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
                &rmeta, methodName, methodTypes.size(), methodTypes.constData());
        break;
    case QSIGNAL_CODE:
            //如果 method 是信号类型,那么使用 QMetaObjectPrivate::indexOfSignalRelative 函数计算信号的相对序号 method_index_relative。
        method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
                &rmeta, methodName, methodTypes.size(), methodTypes.constData());
        break;
    }
    if (method_index_relative < 0) {
        // check for normalized methods
        //格式化
        tmp_method_name = QMetaObject::normalizedSignature(method);
        method = tmp_method_name.constData();

        methodTypes.clear();
        methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes);
        // rmeta may have been modified above
        rmeta = receiver->metaObject();
        switch (membcode) {
        case QSLOT_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(
                    &rmeta, methodName, methodTypes.size(), methodTypes.constData());
            break;
        case QSIGNAL_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(
                    &rmeta, methodName, methodTypes.size(), methodTypes.constData());
            break;
        }
    }
	//两次检测都失败,返回空链接
    if (method_index_relative < 0) {
        err_method_notfound(receiver, method_arg, "connect");
        err_info_about_objects("connect", sender, receiver);
        return QMetaObject::Connection(0);
    }

    //检查信号函数参数个数、类型与接收函数的参数个数、类型是否 能兼容,如果不兼容返回空连接,如果兼容就继续往下走。
    if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(),
                                              methodTypes.size(), methodTypes.constData())) {
        qWarning("QObject::connect: Incompatible sender/receiver arguments"
                 "\n        %s::%s --> %s::%s",
                 sender->metaObject()->className(), signal,
                 receiver->metaObject()->className(), method);
        return QMetaObject::Connection(0);
    }

    int *types = 0;
    //如果连接类型 type == Qt::QueuedConnection,那么使用 queuedConnectionTypes 函数计算入队关联需要的额外类型指针 types
    if ((type == Qt::QueuedConnection)
            && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) {
        return QMetaObject::Connection(0);
    }

#ifndef QT_NO_DEBUG
    //QMetaObjectPrivate::signal 函数根据源头元对象smeta和信号相对序号 signal_index,得到信号元方法 smethod;
    QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
    //计算 method_index_relative + rmeta->methodOffset() ,也就是接收端的元方法绝对序号,然后通过 rmeta->method 函数得到接收端元方法 rmethod;
    QMetaMethod rmethod = rmeta->method(method_index_relative + rmeta->methodOffset());
    //查源头元对象 smeta、元方法 smethod 、接收端元对象 rmeta、元方法 rmethod 是不是具有 QMetaMethod::Compatibility 特性。
    check_and_warn_compat(smeta, smethod, rmeta, rmethod);
#endif
    //)前面全都是参数判断和查询序号,最后才是关键的一步,进行实际的连接操作:
    QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
        sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));
    return handle;
}

可以看到QObject内的connect函数前半部分都是在做类型检测,到最后获取到了几个信息

  • QObject sender 发送者
  • signal_index 信号绝对序号
  • QMetaObject smeta 发送者元对象
  • receiver 接收者
  • method_index_relative 接收者槽函数绝对序号
  • QMetaObject rmeta 接收者元对象
  • Qt::ConnectionType type 槽连接方式
  • types:是多线程程序的入队关联需要的额外类型指针
QObjectPrivate::Connection *QMetaObjectPrivate::connect(const QObject *sender,
                                 int signal_index, const QMetaObject *smeta,
                                 const QObject *receiver, int method_index,
                                 const QMetaObject *rmeta, int type, int *types)
{
    QObject *s = const_cast<QObject *>(sender);
    QObject *r = const_cast<QObject *>(receiver);

    int method_offset = rmeta ? rmeta->methodOffset() : 0;
    Q_ASSERT(!rmeta || QMetaObjectPrivate::get(rmeta)->revision >= 6);
    QObjectPrivate::StaticMetaCallFunction callFunction =
        rmeta ? rmeta->d.static_metacall : 0;
//关联操作需要访问、修改源头和接收端的信号和槽信息,因此需要使用互斥锁,独占源头和接收端信号和槽信息的访问权,先锁定访问权,然后再进行操作。这对于多线程操 作尤为重要,不能多个线程同时操作一对源头和接收端,那样会造成信号和槽数据的混乱,如果情况严重会导致程序崩溃,因此启用互斥锁
    QOrderedMutexLocker locker(signalSlotLock(sender),
                               signalSlotLock(receiver));
    
//关联类型 type 是 Qt::AutoConnection、Qt::DirectConnection、Qt::QueuedConnection 或Qt::BlockingQueuedConnection,这些类型的关联,可以将完全相同的源头、信号、接收端、槽函数关联多次,那样信号触发一次,槽函数会被调用 多次,重复关联是有效的。Qt::UniqueConnection 是唯一关联的标志,可以与其他四种关联标志叠加(OR 运算),那样就会执行上面一段代码,检查关联的唯一性,如果是重复关联就返回 0 ,如果没重复就进行唯一性关联。上面代码就是穷举比较源头信号关联列表 c2 里面有没有重复的关联。一般关联类型 type 都是 Qt::AutoConnection,所以可以不用管上面一段唯一性检查的代码。
    if (type & Qt::UniqueConnection) {
        QObjectConnectionListVector *connectionLists = QObjectPrivate::get(s)->connectionLists;
        if (connectionLists && connectionLists->count() > signal_index) {
            const QObjectPrivate::Connection *c2 =
                (*connectionLists)[signal_index].first;

            int method_index_absolute = method_index + method_offset;

            while (c2) {
                if (!c2->isSlotObject && c2->receiver == receiver && c2->method() == method_index_absolute)
                    return 0;
                c2 = c2->nextConnectionList;
            }
        }
        type &= Qt::UniqueConnection - 1;
    }

    QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection);
    c->sender = s;//发送源头对象;
    c->signal_index = signal_index;//源头信号绝对序号
    c->receiver = r;//接收端对象;
    c->method_relative = method_index;//接收端方法相对序号;
    c->method_offset = method_offset;//接收端元方法偏移;
    c->connectionType = type;//连接类型;
    c->isSlotObject = false;//是否为 SlotObject,新式关联语法和 QML程序会用到这个奇怪的 SlotObject。
    c->argumentTypes.store(types);//保存多线程入队关联需要的 types 指针
    c->nextConnectionList = 0;//nextConnectionList 是下一个链表节点,暂时为 0,以后由 QObjectPrivate::addConnection 函数填充。
    c->callFunction = callFunction;//重点:接收端的私有静态函数 qt_static_metacall(),这个接收端私有静态函数是可以根据相对序号调用元方法的。

    QObjectPrivate::get(s)->addConnection(signal_index, c.data());

    locker.unlock();
    QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
    if (smethod.isValid())
        s->connectNotify(smethod);

    return c.take();
}

QObjectPrivate::addConnection 函数根据信号绝对序号 signal_index 和连接数据 c.data() 添加新的连接。
感兴趣的读者可以跟踪 QObjectPrivate::addConnection 函数源码,这里不贴代码了,一张数据结构图说明:
在这里插入图片描述

从信号到槽函数

信号触发时,调用moc_xx.cpp中的信号函数

void Sig_SLOT::label_number_change(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

其中调用复杂版本的版本

activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv);

这个函数比较复杂,只讲一下比较关键的部分

如果sender与receiver在一个线程,则直接使用如下函数回调槽函数

  callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);

如果与sender与receiver不在同一个线程,使用如下函数构造QMetaCallEvent事件,然后Post到receiver的事件循环中。

    queued_activate(sender, signal_index, c, argv ? argv : empty_argv, 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);
    }

注意:QMetaCallEvent 是一个包含信号及槽相关信息,通过QMetaCallEvent ,接收对象可以在其所在线程中正确执行槽函数。

在这里插入图片描述


版权声明:本文为chengkeke366原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。