QT信号与槽的源码简要分析

信号到槽是QT的标识性特征之一.
本文打算用简明扼要的源码分析,来告诉读者信号与槽是怎么实现的.
不然要分析的太多,又复杂,会让人蒙掉----他说的到底是啥?

本文讲述的是一个类MySender向另一个MyReceiver发信号,并响应的故事.

先来熟悉一下宏吧,混个脸熟:

# define slots
# define signals protected
# define emit  //可忽略
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)

可以看出,这些宏并没有什么特别,但是QT会用moc对slots和signals这两个做特殊处理.可以思考一下,标准的C++并没有信号和槽这个东西的,QT增加了这个东西,他(QT)必然会住类里面添加一些信息.

关键点: 要使用信号与槽功能,必须要继承QObject并在类里面加Q_OBJECT宏.
看看我的MySender类,继承自QObject并在类的第一行里加了Q_OBJECT宏.

class MySender : public QObject
{
    Q_OBJECT

public:
    MySender(){}
    ~MySender(){}

public:
    void UiMakeValueChanged()
    {
        emit valueChanged(100);
    }

signals:
    void valueChanged(int newValue);
};

另外,我在MySender里面定义了一个信号函数void valueChanged(int newValue)
正如你看到的,我并没有实现这个函数,那么谁来实现的呢,答案是QT的moc处理
QT moc处理后,会自动生成一个文件moc_mysender.cpp里面实现了void valueChanged(int newValue),我们看看吧

// SIGNAL 0
void MySender::valueChanged(int _t1)
{
    void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

这个实现先把参数放到一个void*数组里面,然后就调用了QMetaObject::activate.
先说到这里,后面再来分析.

再看看我们的信号接收者的实现

class MyReceiver : public QObject
{
    Q_OBJECT

public:
    MyReceiver(){}
    ~MyReceiver(){}

protected slots:
     void setValue(int value)
     {
         qDebug() << value << "\r\n";
     }
};

实现也比较简单,就是定义了一个槽,然后在里面打印一下参数value.
QT的moc也帮忙实现了一个文件叫moc_myreceiver.cpp.

好了,有了信号,有了槽,还需要做一件事情:connect,把信号和槽连接起来.
看代码:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    MySender s;
    MyReceiver r;

    QObject::connect(&s, SIGNAL( valueChanged(int) ),&r, SLOT( setValue(int)) );

    s.UiMakeValueChanged();

    return a.exec();
}

QObject::connect就作用就是把信号和槽连接起来,四个参数也非常形象:

bool QObject::connect(const QObject *sender, const char *signal,
                      const QObject *receiver, const char *method,
                      Qt::ConnectionType type)

sender:发信号的家伙
signal:发送的信号
receiver:接收信号的家伙,
method:接收信号的处理函数(就是slot)
type:不表,今天只讲最简单的,用默认值AutoConnection,后续有机会讲这个东东.

connect后, 调用s.UiMakeValueChanged就可以发出信号啦,当然,没有意外的话,接收者的slot就会响应信号

    void UiMakeValueChanged()
    {
        emit valueChanged(100); //emit可省略,没啥用,只是好看
    }

让我们来运行一下:

运行成功,100被打印出来了.

那我们现在开始提出问题:
1.connect到底干了什么,他是产生了一个连接(connection)吗?
2.如果产生了一个连接,这个连接存在内存哪里的?
3.当发出(emit)一个信号后,是如何调到接收者的slot的?



源码之前,了无秘密,让我们去看看源码吧,如果你还不会调试源码,请参考:https://netpt.net/forum.php?mod=viewthread&tid=53
我们直接进到QObject::connect这个函数里去看:

四个参数
sender:是指向MySender
signal:是字符串"2valueChanged(int)"
receiver是指向MyReceiver
method:也就是slot是字符串"1setValue(int)"
signal和slot前面的2和1是什么意思呢?还记得宏定义吗?
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)
原来signal和slot都是字符串,QT在前面加了一个"数字"用来识别区分signal和slot.

这段代码主要是将signal字符串转成signal_index,为何要这样呢?
我猜想, 存字符串不利于查找,也更浪费内存,所以转成索引,更加方便.
const QMetaObject *smeta = sender->metaObject();
因为我们的类是继承自QObject,所以就有QObject的特性,QMetaObject有类的信息.
代码我没有截屏完,也没必要全部截屏,紧接着,同理,receiver也千辛万苦地把字符串method转换成了整数method_index_relative
然后,开始执行函数QMetaObjectPrivate::connect

可以看出,QMetaObjectPrivate::connect之前的主要工作就是把字符串转成index,无论是signal,还是method.

QMetaObjectPrivate::connect里面有核心信息:
1.connect到底干了什么,他是产生了一个连接(connection)吗?
是的,从QObjectPrivate::Connection *c = new QObjectPrivate::Connection;可以看出.
2.如果产生了一个连接,这个连接存在内存哪里的?
存在了两个地方,发送者对象实例s和接收者对象实例r都存了.




现在我们来解决另一个问题,当发送者产生信号后,怎么就调用到接收者函数了,
这里只讲最简单的情况,也就是在同一个线程的情况,跨线程会比较复杂,以后有机会再说.
代码执行顺序为:
s.UiMakeValueChanged();->emit valueChanged(100);->void MySender::valueChanged(int _t1)

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

可以看出, 会执行到QMetaObject::activate
三个参数:
this肯定是指的是MySender
staticMetaObject也是MySender的staticMetaObject
a是把参数放到一个数组里面去.
QMetaObject::activate的定义如下:

void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
                           void **argv)

这里面做的事情是把存在sender里面的connection找出来,然后再调用

由于signal和slot是多对多的关系,一个signal可以对应多个slot,一个slot也可以对应多个signal
所以list中的connection个数>=1, 每一个connection都会被处理的.
 

最后,我们来看下调用堆栈

3.当发出(emit)一个信号后,是如何调到接收者的slot的?
a).发出调用valueChanged(100)
b)QMetaObject::activate(this, &staticMetaObject, 0, _a); //this为MySender, &staticMetaObject属于MySender, _a[0] = 0, _a[1] = 100
c).从sender中找到第2)点存起来的connection
d).用connection->callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);进行回调
e).调用到moc生成的代码void MyReceiver::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)

f).调用到我们自己写的代码:void MyReceiver::setValue(int value)
我能发表下感想吗???
在同一线程中,signal&slot就是回调!呃,好吧,不然呢?

代码见:https://gitee.com/flash008/qt_signal_slot/tree/master/000100/

参考:
https://www.cnblogs.com/Vancamel/p/11217966.html
https://www.cnblogs.com/sherlock-lin/p/13960936.html
https://blog.51cto.com/9291927/2070398

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值