解析Qt元对象系统(三) 信号与槽

引述

我们从普通的点击按钮过程来看一下信号与槽的运行机制。先运行一个普通的QWidget程序,添加一个按钮,定义一个槽函数test,函数体里做个断点,调试运行,可以看到从回调函数qt_internal_proc开始,一直到槽函数。元对象系统的调用层次如下

QAbstractButtonPrivate::clickQAbstractButtonPrivate::emitClicked属于源码部分,而且代码比较简单,直接看信号QAbstractButton::clicked

信号函数

前面提到我们声明的信号是由moc工具在moc_.cpp中实现的,例如:

// 有参数的信号
void Widget::valueChanged(double _t1)
{
    void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 2, _a);
}

activate函数

QMetaObject::activate函数是负责联络接收方槽函数的,它根据源头对象指针 this、源头的元对象指针 &staticMetaObject、信号序号 2、信号参数数组_a 去找寻需要激活的槽函数,最终会调用每个关联到该信号的槽函数。

遍历所有receiver并触发它们的slots。针对不同的连接类型,这里的派发逻辑会有不同。看源码:

void QMetaObject::activate(QObject *sender, int signalOffset ......)
{
    . . . . . .
    //发送方和接收方是否在同一个线程中
        const bool receiverInSameThread =  currentThreadId == receiver->d_func()->threadData->threadId;
         //决定连接立刻发送还是加入事件队列
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection))
                {
                queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker);
                continue;
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
                if (receiverInSameThread) { // 如果是BlockingQueued类型且同一线程,报死锁
                    qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
                    "Sender is %s(%p), receiver is %s(%p)",  省略);
                }
                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);
}
    //不是以上情况,则直接调用关联的槽
    . . . . . . 

再看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);

    //post事件到接收者所属线程的QThreadData::postEventList队列中
     QCoreApplication::postEvent(c->receiver, ev);
  ...
}

postEvent第二个参数是QMetaCallEvent,这个signal-slot的connection就发送到receiver的消息队列中,这是线程安全的,通过这种方法Qt实现了跨线程的signal-slot传递。除了信号触发线程与接收者线程相同的情况能直接调用到slot,其它情况都依赖事件机制,也就是说receiver线程必须要有eventloop,否则slot函数是没有机会触发的。

qt_static_metacall()

这个函数是私有静态函数,用于处理当前类的元方法调用以及信号函数相对序号查询,qt_metacall() 会调用这个私有静态函数。

自动关联函数

对于在ui模式中添加的信号,在moc_.cpp中看不到,但是打开ui_.h可以看到一个函数:QMetaObject::connectSlotsByName(MainWindow);,它被称为自动关联函数。自动关联的过程就是根据字符串匹配查找发送源头、信号,然后关联到 object 对象自己的槽函数。发送源头通常是 object 对象自己的内部成员,比如大窗口里面的一堆子控件,子控件的信号关联到大窗口自己的槽函数。

它是一个静态函数,源码在qobject.cpp:

// internal slot-name based connect
static void connectSlotsByName(QObject *o); // o可以是MainWindow

代码太复杂,大致过程是:
1. 将QObject对象的children和自身加入对象列表。
2. 获取元对象mo和包含的所有元方法,检查其名称是不是以on_开头,如果不是则跳过,如果是则进入内层循环。
3. 对对象列表中的名称和slot里的signal name对比,如果匹配则查找发送端信号的绝对序号sigIndex,找到后调用QMetaObjectPrivate::connect实现关联,跳出循环。

四种连接类型:

QueuedConnection:sender和receiver不在同一线程,sender向receiver所在线程的消息循环发送事件,此事件得到处理时会调用slot,像Win32的::PostMessage。

BlockingQueuedConnection:处理方式和QueuedConnection相同,但发送信号的线程会等待信号处理结束再继续,像Win32的::SendMessage。

DirectConnection:在当前线程直接调用receiver的slot,这种类型无法支持跨线程的通信。

AutoConnection:信号发射时的当前线程和槽函数关联的线程不一致时,用QueuedConnection方式,否则用DirectConnection。

sender函数在slot函数中调用可以获取发送该signal的对象,但仅用于同一线程的Qt::DirectConnection连接的 signal。由于这种做法破坏了面向对象的原则,慎用!

其他

Qt4中的信号与槽匹配是在运行时检查的,Qt5引入了函数指针关联语法,这样可以在编译期间检查。

QObject::deletelater() 。从源码可以看出,这个调用也只是发送了一个事件,等对象所属线程的消息循环获取控制权来处理这个事件时做真正的delete操作。
所以调用这个方法要谨慎,确保对象所属线程具有激活的eventloop,不然这个对象就被泄露了

Qt5中,信号槽是有返回值的。只是Qt的一个信号可以连接多个槽,还有同步调用和异步调用的问题,没发支持的很好,所以,返回值虽有,但只是鸡肋。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值