• 作者:老汪软件技巧
  • 发表时间:2024-09-21 11:01
  • 浏览量:

这篇文章如下看懂了,面试的时候如果面试官问道关于信号和槽的原理,那就把细节告诉他。让他知道是有能力看源码的,不是简单的背背八股文就完事了的.

1. 测试代码

给出一段测试代码,用于后面的信号、槽源码分析

class A

class A : public QObject
{
    Q_OBJECT
public:
    A() = default;
    ~A() = default;
signals:
    void Signal_A_one();    // 信号1
    void Signal_A_two();    // 信号2
    void Signal_A_three();  // 信号3
public slots:
    void Slot_A_one()    // 槽函数1
    {
        qDebug() << "slot a one";
    }
    void Slot_A_two()    // 槽函数2
    {
        qDebug() << "slot a two";
    }
};

class B

class B : public QObject
{
    Q_OBJECT
public:
    B() = default;
    ~B() = default;
signals:
    void Signal_B_one();    // 信号1
    void Signal_B_two();    // 信号2
    void Signal_B_three();  // 信号3
public slots:
    void Slot_B_one()    // 槽函数1
    {
        qDebug() << "slot a one";
    }
    void Slot_B_two()    // 槽函数2
    {
        qDebug() << "slot a two";
    }
};

连接代码,这里将类A的信号绑定三个槽函数,使用了三种连接方式

A aa;
B bb;
// 三种connect连接方式
QObject::connect(&aa, &A::Signal_A_one, &bb, &B::Slot_B_one);
QObject::connect(&aa, SIGNAL(Signal_A_one), &bb, SLOT(Slot_B_two));
QObject::connect(&aa, &A::Signal_A_one, []{ qDebug() << "lambda slot"; });

2. connect函数源码分析2.1 类分析

connection函数底层会使用到几个类,有必要先了解一下,防止后面看源码不懂

QObject

class QObject 
{ 
protected:
    QScopedPointer d_ptr;
}

QObject只有这一个成员,成员类型是QObjectData类型

还有一个类QObjectPrivate继承自这个对象,在Qt源码中经常会用到这个类

class QObjectPrivate : public QObjectData
{
public:
    struct ConnectionOrSignalVector;
    struct Connection : public ConnectionorOrSignalVector;
    struct ConnectionList;
    struct Sender;
    struct SignalVector : public ConnectionOrSignalVector;
    struct ConnectionData;
    
public:
// 成员
    QAtomicPointer threadData;
    QAtomicPointer connections;
    
    union {
        QObject* currentChildBeingDeleted;
        QAbstractDeclartiveData* declarativeData;
    };
};

这个类里面定义了很多类,下面逐一介绍,以及每个成员变量的作用是什么:

ConnectionOrSignalVector

struct ConnectionOrSignalVector
{
    union {
        ConnectionOrSignalVector* nextInOrphanList;
        Connection* next;    // 指向下一个连接对象
    };
};

ConnectionOrSignalVector是很多类的基类,这里面定义了一个联合体,其中next是用来指向下一个Connection连接对象的

Connection连接对象,这个类里面存储着关于发送者和接收者双方连接的一些信息,有当前线程数据、元调用函数、信号偏移值等.

struct Connection : public ConnectionOrSignalVector
{
    // 二级指针,如果是最新绑定的,那么指向receiver其中的sender
    // 否则就是指向上一个绑定的Connection对象
    Connection** prev;    
    QAtomicPointer nextConnectionList;    // 指向下一个Connection
    Connectin* prevConnectionList;   // 指向上一个Connection
    
    QObject* sender;   // 发送者对象
    QAtomicPointer receiver;    // 接收者对象
    QAtomicPointer receiverThreadData;    // 当前线程的数据
    
    // 指向元调用函数,使用staticMetaObject对象还是qt_static_metacall函数
    union {
        StaticMetaCallFunction callFunction; 
        QtPrivate::QSlotObjectBase* slotObj;
    };
    
    QAtomicPointer<const int> argumentTypes;
    QAtomicInt ref_;    // 引用计数,默认为2(发送者和接收者)
    uint id = 0;
    ushort method_offset;
    ushort method_relative;
    signed int signal_index : 27;  // 信号在继承体系中的偏移值
    ushort connectionType : 3;     
    ushort isSlotObject : 1;
    ushort ownArgumentTypes : 1;
};

上面只列出几个相对来说咱们感兴趣的成员,这里就不一一描述每个都是做什么的,只要知道这个类是有关连接的详细信息.

接下来就是存储连接信息数组的有关类,Qt底层会创建一个堆内存数组,这块内存的头部是SignalVector类型,剩余后面就是ConnectionList类型.

SignalVector记录有关这个数组的大小.ConnectionList是一个链表的结点信息,有first和last成员.搞明白这个需要等到下面的addConnection函数解析就明白了.

struct SignalVector : public ConnectionOrSignalVector 
{
    quintptr allocated;
};

struct ConnectionList 
{
    QAtomicPointer first;
    QAtomicPointer last;
};

ConnectionData这个是QObjectPrivate类的成员.发送者或者接收要获取关于连接的信息,都要获取这个对象,它里面是保存着所有连接信息.

struct ConnectionData
{
    QAtomicInteger currentConnectionId;
    QAtomicInt ref;
    QAtomicPointer signalVector;   // 信号向量
    Connection *senders = nullptr;    // 发送者
    Sender *currentSender = nullptr;   
    QAtomicPointer orphaned;
};

2.2 函数源码分析

qt提供了三种连接方式,分别使用函数指针、宏、lambda,这俩就不再特别细的讲每种的区别,只要最底层核心细节,是怎么进行连接的.

这里就以函数指针的连接方式,使用这个来进行分析讲解,三种方式底层都是一样的.所以看懂了一种就可以了

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)
{
    // 忽略检查判断代码
    QObject *s = const_cast(sender);
    QObject *r = const_cast(receiver);
    
    // 给发送者和接收者都上锁,保证线程安全
    QOrderedMutexLocker locker(signalSlotLock(sender),
                           signalSlotLock(receiver));
    if (type & Qt::UniqueConnection && slot && QObjectPrivate::get(s)->connections.loadRelaxed()) 
    {
        QObjectPrivate::ConnectionData *connections = 
            QObjectPrivate::get(s)->connections.loadRelaxed();
        if (connections->signalVectorCount() > signal_index) 
        {
            const QObjectPrivate::Connection *c2 = 
                connections->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed();
            while (c2) 
            {
                if (c2->receiver.loadRelaxed() == receiver && 
                    c2->isSlotObject && c2->slotObj->compare(slot)) 
                {
                    slotObj->destroyIfLastRef();
                    return QMetaObject::Connection();
                }
                c2 = c2->nextConnectionList.loadRelaxed();
            }
        }
        type = static_cast(type ^ Qt::UniqueConnection);
    }
}
// 后续代码在下面讲解

第7~8行,去掉const属性,因为后面需要更改这两个指针,这俩指针分别是发送者对象和接收者对象

第11~12行,获取两把锁,用于发送者和接收者,保证连接过程中是线程安全的.

第13~33行,如果指定了是唯一连接UniqueConnetion,那么则只能有一个同样的连接.比如如下代码,是可以重复连接,那么发射信号的时候也就执行两次槽函数

QObject::connect(&aa, &A::Signal_A_one, &bb, &B::Slot_B_one);
QObject::connect(&aa, &A::Signal_A_one, &bb, &B::Slot_B_one);
// 这里会调用两次B::Slot_B_one
emit aa.Signal_A_one();

第13~33行详细分析如下:

13行: 如果连接类型指定了UniqueConnection代表只能有这唯一连接,不能重复. 然后接着判断之前是否已经有个连接信息

15~16行: 获取连接对象信息,该对象保存着所有的连接信息

17行: 判断当前信号向量数组的大小是否大于参数传递的信号下标,如果大于,那么则有可能之前已经有过关于这个信号和某个槽的连接

19行: 获取关于这个信号下标在信号向量数组中对应的连接对象信息Connection

22行: 如果循环条件成立,那么遍历这个链表,看是否以前有过跟当前相同的连接信息,如果有的话最终28行返回退出函数

[第二段] 创建连接新消息,并加到信号向量数组中,这都是在发送者对象数据里进行操作

std::unique_ptr c{new QObjectPrivate::Connection};
c->sender = s; // 设置发送者对象
c->signal_index = signal_index; // 设置信号索引,这个是在整个继承体系中的索引
QThreadData *td = r->d_func()->threadData; // 获取当前线程的数据
td->ref(); // 增加引用计数
c->receiverThreadData.storeRelaxed(td); // 存储线程数据
c->receiver.storeRelaxed(r); // 设置接收者对象
c->slotObj = slotObj; // 设置槽函数指针
c->connectionType = type; // 设置连接类型
c->isSlotObject = true;
if (types) // 如果连接类型是队列或者阻塞类型,那么types就是槽函数中的参数类型列表
{
// 存储参数的类型
    c->argumentTypes.storeRelaxed(types);
    c->ownArgumentTypes = false;
}
QObjectPrivate::get(s)->addConnection(signal_index, c.get());
QMetaObject::Connection ret(c.release());
locker.unlock();
QMetaMethod method = QMetaObjectPrivate::signal(senderMetaObject, signal_index);
Q_ASSERT(method.isValid());
s->connectNotify(method);
return ret;

第1行: 创建一个Connection对象,用来设置有关此次连接的一些信息,比如发送者、接收者、信号和槽的一些信息

第2~10行: 设置有关连接的信息,每个作用看上面代码注释

qt信号槽实例__qt自定义信号和槽

第18~24行: 就是底层的核心,用来添加连接对象.其中核心函数就是addConnection,这个函数就是操作上面讲到的那些类

第18行: 调用QObjectPrivate类的addConnection函数,传递两个参数,分别是信号索引和创建的连接对象,我们进去看这个函数具体做了什么

void QObjectPrivate::addConnection(int signal, Connection *c)
{
    ensureConnectionData();    // 如果没有ConnectionData对象则创建一个
    ConnectionData *cd = connections.loadRelaxed();
    cd->resizeSignalVector(signal + 1); // 创建信号向量数组
    
    // signal信号索引作为数组下标,互殴信号向量数组中的ConnectionList对象
    ConnectionList &connectionList = cd->connectionsForSignal(signal);
// 下面就是存储Connection的细节了
    if (connectionList.last.loadRelaxed()) 
    {
        Q_ASSERT(connectionList.last.loadRelaxed()->receiver.loadRelaxed());
        connectionList.last.loadRelaxed()->nextConnectionList.storeRelaxed(c);
    } 
    else 
        connectionList.first.storeRelaxed(c);
    c->id = ++cd->currentConnectionId;
    c->prevConnectionList = connectionList.last.loadRelaxed();
    connectionList.last.storeRelaxed(c);
    QObjectPrivate *rd = QObjectPrivate::get(c->receiver.loadRelaxed());
    rd->ensureConnectionData();
    c->prev = &(rd->connections.loadRelaxed()->senders);
    c->next = *c->prev;
    *c->prev = c;
    if (c->next)
        c->next->prev = &c->next;
}

可以看到代码量非常少,其实代码逻辑也很简单

第3行: 如果当前从来没有创建过ConnectionData对象,那么则调用ensureConnectionData创建一个,然后存储到QObjectPrivate类中的connections成员

第4行: 获取这个ConnectionData对象

第5行: 创建一个信号向量数组,大小是信号索引 + 1,这个函数代码细节如下:

第8行: 获取当前索引下标在信号向量数组中的ConnectList对象

第10~16行: 如果之前设置过last,那么将这次的Connection对象存储在connectList.last->nextConnectionList中,否则存储在connectionList.first中

第18行: 设置prevConnectionList指向上一个存储的连接对象信息

第19行: 设置connectionList.last存储为当前连接对象

第21~22行: 获取接收者的QObjectPrivate对象,并为接收者创建ConnectionData对象

第24行: 当前连接对象的prev指向接收者ConnectionData中的senders成员

第25行: 设置当前连接对象的next为接收者ConnectionData中sender指向的对象.如果是第一次连接,这个next会被设置为空.否则,这个next设置就是接收者中的sender

第26行: 这一行代码,就是将接收者的sender指向当前连接对象

第27~28行: 将上一个连接对象的prev指向自己

可能通过上面这些文字看不出来到底怎么连起来的,下面画个图增加清晰的认知,看底层这个信号向量和连接是怎么连接的

根据上面的连接代码

连接信息如下,一个信号和三个槽函数连接

第一次连接:

会给发送者创建信号向量数组,存储在QObjectPrivate类中的成员connections中,该成员类型为QAtomicPointer

根据signal_index获取信号向量数组中对应下标的ConnectionList对象

然后设置相应的指针,第一次连接如下图所示连接:

第二次连接:

这次再次同样的信号连接不同的槽函数

这回就清晰明了了,新连接相当于链表的头,然后和上一次连接连起来.后面关于这个信号的多次连接也都是这样.

注意: 结点的连接也都是线程安全的

所以这里引申出一道面试题,那就是qt的信号和槽底层是怎么实现的.

每个QObject类都有一个类型为QObjectData的指针成员d_ptr.QObjectPrivate继承自这个,所以可以把这个指针转换成QObjectPrivate类型.

我们会在发送者的QObjectPrivate成员对象中,创建一个信号向量数组,这个数组大小是根据信号在整个继承体系中的索引来确定的.数组的每个元素都是ConnectionList类型,该类型就是一个链表结点,这个结点的first存储第一次连接的对象信息, last存储最后一次连接的对象信息.

每次连接都会创建一个Connection对象,该对象存储着有关连接的信息,比如发送者对象、接收者对象、信号索引、发送者的元方法、槽函数等等. 最新创建的Connection相当于链表的头,next指向上一次创建的Connection对象.链表头指向接收者中的sender成员.

根据发送者的信号索引获取信号向量数组中对应下标的ConnectionList对象,然后设置first和last指向第一次连接和最后一次连接的对象.

这也就构成了每个信号都有属于自己的链表,每个结点都是这个信号和什么槽函数绑定的信息.

3. emit发射信号是如何执行绑定的槽函数的

我们有了上面的链表信息,其实触发信号就是遍历这些链表,使用接收者对象指针调用对应的槽函数,如果有参数,则把参数也传递进去.对于跨线程会使用队列保证异步安全的,具体函数细节就不再分析了.

这里只讲下emit到底是什么东西,居然可以使槽函数执行

emit aa.Signal_A_one(1, 1.1);

emit本质上是个空宏,什么都没有.上面这段代码,就是调用Signal_A_one函数,这个函数是moc编译器帮我们生成的.moc编译器emit,就会在moc文件里生成一个一个函数

// SIGNAL 0
void A::Signal_A_one(int _t1, double _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))), 
        const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t2))) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

最终会调用active函数,这个函数就是遍历发送者的连接链表,然后根据接收者对象指针调用对应的槽函数.

而且信号和槽函数是线程安全的.内部有处理有关不是同一个线程的方法,使用事件队列异步激活槽函数