Qt - 一文理解信号槽机制(万字剖析整理)

目录

  • 简介
  • 简单使用
    • QtDesigner
    • 手动添加
  • connect
    • 三种写法
      • 元方法式
      • 函数指针式
      • functor式
    • 连接类型
    • 信号-槽的元调用
    • 元类型
    • 返回值
  • 元对象编译器moc
    • Q_OBJECT宏展开
    • 用户定义信号
  • 对象之间的通信机制
    • 直接调用
    • 回调函数+映射表
    • 观察者模式
  • 连接过程源码分析
    • connectSlotsByName()
    • connect()
  • 槽函数调用过程
    • 虚伪的emit
    • 源码分析
    • 槽函数调用过程总结
  • 参考鸣谢

简介

信号与槽(Signal & Slot)是 Qt 编程的基础,也是 Qt 的一大创新。因为有了信号与槽的编程机制,在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。

信号(Signal)就是在特定情况下被发射的事件,例如 PushButton 最常见的信号就是鼠标单击时发射的 clicked() 信号,一个 ComboBox 最常见的信号是选择的列表项变化时发射的 CurrentIndexChanged() 信号。

槽(Slot)就是对信号响应的函数。槽是一个函数,与一般的 C++ 函数一样,可以定义在类的任何部分(publicprivateprotected),可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。

简单使用

QtDesigner

QtDesigner 中设置是非常容易的,右键点击你所需要的设置的 控件 -> 转到槽 -> 选择触发信号 -> 编写处理函数 即可。

那么Qt为我们做了什么?

QtDesigner 设计的 *.ui 文件可以通过 uic工具 转换为 ui_*.h文件

mainwindow.ui 为例,每个 *.ui 文件编译后均会生成相应的 ui_xxx.h;该文件一般不会出现在工程目录中。

注意ui_xxx.h 是根据UI设计器可视化设计自动生成的,由于再次编译会重新生成,对其进行手动修改并无意义。

ui_xxx.h 一般包含以下内容:

  • 定义了一个类 ui_xxxx,用于封装可视化设计的界面。

  • 自动生成了界面各组件成员变量的定义。在 public 部分为界面的每个组件定义一个指针变量,类名就是我们在UI可视化设计界面中设置的 ObjectName

  • 定义了 setupUi() 函数,相信对该函数会很眼熟,它在 mainwindow.cpp 中会被调用。控件将在此函数中被创建,然后我们就可以使用 ui->xxx 的形式使用它们。

  • setupUi() 还调用了 retranslateUi(Widget),用来设置界面各组件的文字内容属性,如标签的文字、按键的文字、窗体的标题等。将界面上的文字设置的内容独立出来作为一个函数 retranslateUi(),在设计多语言界面时会用到这个函数。

  • setupUi() 还调用了一行看起来比较特别的代码:QMetaObject::connectSlotsByName(MainWindow);

    Qt - 一文理解信号槽机制(万字剖析整理)_第1张图片
    递归搜索给定对象的所有子对象,并将匹配的信号从它们连接到遵循以下形式的对象插槽:

    void on_<object name>_<signal name>(<signal parameters>);
    

手动添加

借助 QMetaObject::connectSlotsByName() 的作用,按以下命名规则定义槽函数。至于连接就交给 connectSlotsByName,当然这个方法要我们的 ui_xxx.cpp 中存在这个控件(这样偷懒是有代价的,有一点点的性能损耗)。

void on_<object name>_<signal name>(<signal parameters>);

对于自定义控件等,就不能这样偷懒了,需要我们自己定义槽函数并进行 connect

对于Qt自有的控件,若我们自己写 connect 需要注意的是:由于 QMetaObject::connectSlotsByName() 的作用,槽函数的名称需要注意,不要和默认名称相似或相同。轻则警告,重则自动帮我们进行连接,于是信号触发时会调用两次,一次是我们手动连接的,一次是 connectSlotsByName 连接的。

connect

三种写法

元方法式

该方法最为常见,但是它编译时不做错误检查。

    connect(ui->pushButton,SIGNAL(clicked()),this,SLOT(pbutton_clicked()));

函数指针式

    connect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::pbutton_clicked);

较上一种方式,该写法省去了字符查找的过程因此会更快一些。连接建立后,从信号触发到槽函数的执行,两种写法是没有区别的。

该方法较为简洁,而且在编译过程中会对函数参数类型、个数做检查。美中不足的是,若涉及被重载的函数,将非常不优雅!

参考 Differences between String-Based and Functor-Based Connections 实现:

    QLCDNumber::display(int)
    QLCDNumber::display(double)
    QLCDNumber::display(QString)

    auto slider = new QSlider(this);
    auto lcd = new QLCDNumber(this);

    // String-based syntax
    connect(slider, SIGNAL(valueChanged(int)),
            lcd, SLOT(display(int)));

    // Functor-based syntax, first alternative
    connect(slider, &QSlider::valueChanged,
            lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display));

    // Functor-based syntax, second alternative
    void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
    connect(slider, &QSlider::valueChanged,
            lcd, mySlot);

    // Functor-based syntax, third alternative
    connect(slider, &QSlider::valueChanged,
            lcd, QOverload<int>::of(&QLCDNumber::display));

    // Functor-based syntax, fourth alternative (requires C++14)
    connect(slider, &QSlider::valueChanged,
            lcd, qOverload<int>(&QLCDNumber::display));

注意:在一些需要 运行期反射 的情况下(头文件都没有,只知道有这么个对象,和函数的名字),只能用元方法式的写法。

functor式

连接信号到任意 Lambdastd::bind 上,下面是 Lambda 表达式示例:

QByteArray page = ...;
QTcpSocket *socket = new QTcpSocket;
socket->connectToHost("qt-project.org", 80);
QObject::connect(socket, &QTcpSocket::connected, this, [=] () {
        socket->write("GET " + page + "\r\n");
    }, Qt::AutoConnection);

注意

  • 槽函数参数个数可以小于信号参数个数,槽函数参数必须在信号中找到对应,多余的参数将被忽略。
    connect(sender, SIGNAL(mySignal(int, const QString &)),receiver, SLOT(mySlot(int)));
    
  • 信号可以连接信号(后面会分析为什么)。
  • 如果一个信号被多个槽连接,则在发出信号时,将按照连接的顺序执行槽函数
  • disconnect 断开连接。之前在看 Qt图表 Callout例程中就发现 为避免重复connect(),每次绑定前都先进行disconnect() 的写法。

连接类型

直接戳 Qt - 一文理解QThread多线程(万字剖析整理)- connect 部分

Qt - 一文理解信号槽机制(万字剖析整理)_第2张图片

信号-槽的元调用

信号槽特殊的地方,是 moc(元对象编译器) 为其生成了一份 元信息 ,可以通过 QMetaObject::invokeMethod 的方式调用。平时我们不需要用到,但在”运行期反射”的情况下(头文件都没有,只知道有这么个对象,和函数的名字)就需要它了。

QMetaObject::invokeMethod(&mSwitch, "change");

注意:Qt元对象系统不是本文的重点,由于无法绕开它。这里仅引出一个问题,Qt中信号槽机制使用的前提是什么?

元类型

信号槽机制与元对象系统关系较为紧密。QObject 的子类本身已经附带了元信息,我们可以直接调用信号槽。

对于基础数据类型,Qt已经注册进元系统了,我们可以到QMetaType中查看,对于自定义的结构体,类等,则需要注册元类型。

Q_DECLARE_METATYPE(MyStruct)
Q_DECLARE_METATYPE(MyNamespace::MyStruct)

若采用队列链接的 connect,则还需要使用 qRegisterMetaType()注册类型:

With queued connections, the parameters must be of types that are known to Qt's meta-object system, because Qt needs to copy the arguments to store them in an event behind the scenes.

#include 
Q_DECLARE_METATYPE(MyStruct);
qRegisterMetaType<MyStruct>("MyStruct");

返回值

connect() 成功连接将返回连接的句柄 QMetaObject::Connection,若失败则返回无效的句柄。可以将句柄转换为布尔变量进行判断。
Qt - 一文理解信号槽机制(万字剖析整理)_第3张图片

元对象编译器moc

不知你是否有过困惑,为什么使用信号槽的机制必须是 QObject 的直接或间接子类,且必须加上宏 Q_OBJECT

其实,Qt程序的编译,在使用我们 Kits 中设置的编译器之前,还需要经过元对象编译器moc。这个编译器已经被集成到 qmake 当中。

moc编译器解析一些C/C++不存在的关键字,处理 Q_OBJECT 等相关宏,并生成 moc_xxx.cpp 文件。

moc预处理
具体内容
编译程序
moc_xxx.cpp
Q_BJECT展开生成函数声明&moc生成实现
普通编译

下面是我生成的 moc_mainwindow.cpp

/****************************************************************************
** Meta object code from reading C++ file 'mainwindow.h'
**
** Created by: The Qt Meta Object Compiler version 67 (Qt 5.9.3)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/

#include "../../test1/mainwindow.h"
#include 
#include 
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'mainwindow.h' doesn't include ."
#elif Q_MOC_OUTPUT_REVISION != 67
#error "This file was generated using the moc from 5.9.3. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif

QT_BEGIN_MOC_NAMESPACE
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
struct qt_meta_stringdata_MainWindow_t {
    QByteArrayData data[5];
    char stringdata0[56];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_MainWindow_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = {
    {
QT_MOC_LITERAL(0, 0, 10), // "MainWindow"
QT_MOC_LITERAL(1, 11, 11), // "user_signal"
QT_MOC_LITERAL(2, 23, 0), // ""
QT_MOC_LITERAL(3, 24, 21), // "on_pushButton_clicked"
QT_MOC_LITERAL(4, 46, 9) // "user_slot"

    },
    "MainWindow\0user_signal\0\0on_pushButton_clicked\0"
    "user_slot"
};
#undef QT_MOC_LITERAL

static const uint qt_meta_data_MainWindow[] = {

 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       3,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    0,   29,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       3,    0,   30,    2, 0x08 /* Private */,
       4,    0,   31,    2, 0x08 /* Private */,

 // signals: parameters
    QMetaType::Void,

 // slots: parameters
    QMetaType::Void,
    QMetaType::Void,

       0        // eod
};

void MainWindow::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        MainWindow *_t = static_cast<MainWindow *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->user_signal(); break;
        case 1: _t->on_pushButton_clicked(); break;
        case 2: _t->user_slot(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            typedef void (MainWindow::*_t)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MainWindow::user_signal)) {
                *result = 0;
                return;
            }
        }
    }
    Q_UNUSED(_a);
}

const QMetaObject MainWindow::staticMetaObject = {
    { &QMainWindow::staticMetaObject, qt_meta_stringdata_MainWindow.data,
      qt_meta_data_MainWindow,  qt_static_metacall, nullptr, nullptr}
};


const QMetaObject *MainWindow::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

void *MainWindow::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, qt_meta_stringdata_MainWindow.stringdata0))
        return static_cast<void*>(this);
    return QMainWindow::qt_metacast(_clname);
}

int MainWindow::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QMainWindow::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 MainWindow::user_signal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
QT_WARNING_POP
QT_END_MOC_NAMESPACE

Q_OBJECT宏展开

/* qmake ignore Q_OBJECT */
#define Q_OBJECT \
public: \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

QMetaObject 是元对象,元对象系统是Qt另外一套核心机制。在这里,我们仅需要知道这个类存储了父类的源对象、我们当前类描述、函数描述和 qt_static_metacall 函数地址即可。

下面我们逐个看宏展开增加的内容:

  • QMetaObject staticMetaObject
    构造一个 QMetaObject 对象,传入当前moc文件的动态信息。

  • QMetaObject ※metaObject()
    注意:※ = *

    返回当前 QMetaObject,一般而言,虚函数 metaObject() 仅返回类的 staticMetaObject 对象。

  • qt_metacall(QMetaObject::Call, int, void)
    调用函数回调,该函数被异步处理信号时调用,或者Qt规定的有一定格式的槽函数(on_xxx_signalname())触发。在我的 moc_mainwindow.cpp中查看,发现其内部还是调用了 qt_static_metacall

    int MainWindow::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
    {
        _id = QMainWindow::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;
    }
    
  • qt_static_metacall(QObject ※, QMetaObject::Call, int, void )
    注意:※ = *

    根据函数索引进行槽函数的调用,有趣的是自行定义的信号也在其索引中,此为信号信号连接合法性佐证二。

    void MainWindow::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
    {
        if (_c == QMetaObject::InvokeMetaMethod) {
            MainWindow *_t = static_cast<MainWindow *>(_o);
            Q_UNUSED(_t)
            switch (_id) {
            case 0: _t->user_signal(); break;
            case 1: _t->on_pushButton_clicked(); break;
            case 2: _t->user_slot(); break;
            default: ;
            }
        } else if (_c == QMetaObject::IndexOfMethod) {
            int *result = reinterpret_cast<int *>(_a[0]);
            {
                typedef void (MainWindow::*_t)();
                if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MainWindow::user_signal)) {
                    *result = 0;
                    return;
                }
            }
        }
        Q_UNUSED(_a);
    }
    

用户定义信号

如果你跟踪过 signals,会发现其是是一个函数声明:

#     define signals Q_SIGNALS
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)

#ifndef QT_ANNOTATE_ACCESS_SPECIFIER
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
#endif

那么为什么我们可以可以只声明不实现呢? 不过有moc为我们负重前行罢了~
Qt - 一文理解信号槽机制(万字剖析整理)_第4张图片

moc_mainwindow.cpp 中发现自定义的信号的实现:

// SIGNAL 0
void MainWindow::user_signal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

对象之间的通信机制

要提供一种对象之间的通信机制。这种机制,要能够给两个不同对象中的函数建立映射关系,前者被调用时后者也能被自动调用。

再深入一些,两个对象都互相不知道对方的存在,仍然可以建立联系。甚至一对一的映射可以扩展到多对多,具体对象之间的映射可以扩展到抽象概念之间。

理解这些概念对我们查看Qt信号槽实现的源码大有裨益。

假设现在有一个 Dog 类和一个 Host 类,Host 中有一个 breakdown() 方法;Dog中有一个 WanWan() 方法。

显然我们是想实现当房屋遭到破坏时,看门狗发出WanWan警报。

直接调用

在该场景中,若要采用直接调用的方式,则需要在Host类中创建一个Dog类,并在 breakdown() 中调用Dog的 WanWan() 方法。

class Host{

    Dog * m_dog;

    void breakdown(){
        m_dog.WanWan();
    }
}

这种方法存在以下问题:

  • 若无法直接取得Dog对象,该方案亦将pass(如果这是一条流浪狗,我们将无计可施)。
  • 不具备通用性,若存在多只狗,需要实现房屋被破坏时都警报,那么我们需要在Host内部增加对象。
  • 耦合性++。

回调函数+映射表

我们将 WanWan() 函数存在某个地方,采用 Map 保存映射关系,创建一个 Connections 类进行管理。

class Connections{
    void connect(const std::string &name, const std::function<void()> &callback){
        m_callbackMap[name] = callback;
    }

    void run(){
        auto cur = m_callbackMap.find(name);
        //迭代器判断
        if (cur != m_callbackMap.end()) {
            //迭代器有效的情况,直接调用
            cur->second();
        }
    }
private:
    std::map<std::string, std::function<void()>> m_callbackMap;
}

//保存上下文连接
static Connections mConnections;

class Host{
	void breakdown(){
        cout << "The house was damaged." << endl;
        mConnections.run();
    }
}

class Dog(){
    void Dog(){
        mConnections.connect("Dog",std::bind(&Dog::WanWan, this));
    }
    void WanWan(){
        cout << "WanWan..." << endl;
    }
}

//Main
void main(){
    Host mHost;
    mHost.breakdown
}

这样当我们的Host调用 breakdown() 时,与之绑定的Dog将 WanWan...

问题1-2 也得以解决,但是由于监听的动作是在Dog构造函数中实现的,耦合性还是比较高。

观察者模式

思路与上一种方法类似,我们将Host和Dog抽象出来,形成目标类 Subject 和观察者类 Observer,在目标类中存在一私有变量保存与之绑定的观察者。 这一模型又被称为 发布-订阅模型。可以很容易地实现目标对象的状态变化通知到所用已绑定的观察者。

//抽象类-观察者
class Observer{
	virtual  void update();
}

//具体观察者
class Dog :Observer{
	void update(){
		count << "WanWan..." << endl;
	}
}

//目标类-主题
class Subject{

private:
	vector<Observer> oVector = new Vector<>();

	void add(Observer &server){
		oVector.add(server);
	}

	bool remove(Observer &server){
		return oVector.delete(server);
	}

	//通知所有观察者
	void notityObserver(){
	for(Observer observer : oVector) {
		observer.update();
	}
}

//目标对象
class Host:Subject{
	void breakdown(){
		cout << "The house was damaged." << endl;
		notityObserver();
	}
}

int main(int argc, char const *argv[])
{
	Host host = new Host;
	Dog dog1 = new Dog;
	Dog dog2 = new Dog;
	host.add(dog1);
	host.add(dog2);

	host.breakdown();
	return 0;
}

连接过程源码分析

connectSlotsByName()

void QMetaObject::connectSlotsByName(QObject *o)
{
    //... 
    // 遍历对象中的每个槽方法
    for (int i = 0; i < mo->methodCount(); ++i) {
        //...
        //检查列表中的每个对象
        for(int j = 0; j < list.count(); ++j) {
            // "on__"规则匹配
            //...
            //匹配到了相应的槽
            if (Connection(QMetaObjectPrivate::connect(co, sigIndex, smeta, o, i))) {
                foundIt = true;
                break;
            }
        //...
         }
    }
}

可以发现它最后调用了 QMetaObjectPrivate::connect()

由于需要遍历匹配,相比直接 connect() 于程序整体而言都损失了一部分效率(偷懒的代价)。

connect()

跟踪 connect(this,SIGNAL(user_signal()),this,SLOT(user_slot())); 中的 connect()

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
                                     const QObject *receiver, const char *method,
                                     Qt::ConnectionType type)
{
    //...
    QByteArray tmp_signal_name;
    // 【1】检查是否为信号 
    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);
    //获取函数index
    int signal_index = QMetaObjectPrivate::indexOfSignalRelative(
            &smeta, signalName, signalTypes.size(), signalTypes.constData());
    //...
    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_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;
    }
    //...
    //【2】
    QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
        sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types));
    return handle;
}

【1】check_signal_macro()检测是否为信号的实现:

static bool check_signal_macro(const QObject *sender, const char *signal,
                                const char *func, const char *op)
{
    int sigcode = extract_code(signal);
    if (sigcode != QSIGNAL_CODE) {
        if (sigcode == QSLOT_CODE)
            qWarning("QObject::%s: Attempt to %s non-signal %s::%s",
                     func, op, sender->metaObject()->className(), signal+1);
        else
            qWarning("QObject::%s: Use the SIGNAL macro to %s %s::%s",
                     func, op, sender->metaObject()->className(), signal);
        return false;
    }
    return true;
}

static int extract_code(const char *member)
{
    // extract code, ensure QMETHOD_CODE <= code <= QSIGNAL_CODE
    return (((int)(*member) - '0') & 0x3);
}

#define QSLOT_CODE    1
#define QSIGNAL_CODE  2

也就是说Qt是通过 char *signal 中的字符判断来确实它是不是信号的,1xxx 表示槽,2xxx 表示信号。

【2】是我们的重点,了解下面的代码需要先了解 观察者模式

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

    /*
    moc文件中的qt_static_metacall函数的地址被传递给了staticMetaObject对象的static_metacall域。
    这里又将static_metacall域的内容传递给callFunction。所以callFunction指向了moc文件中的qt_static_metacall函数。
    */
    QObjectPrivate::StaticMetaCallFunction callFunction = rmeta ? rmeta->d.static_metacall : nullptr;

    //...
    //connection对象,监听者模式
    std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
    c->sender = s;
    c->signal_index = signal_index;
    c->receiver.storeRelaxed(r);
    QThreadData *td = r->d_func()->threadData;
    td->ref();
    c->receiverThreadData.storeRelaxed(td);
    c->method_relative = method_index;
    c->method_offset = method_offset;
    c->connectionType = type;
    c->isSlotObject = false;
    c->argumentTypes.storeRelaxed(types);
    c->callFunction = callFunction;
    //【3】添加监听
    QObjectPrivate::get(s)->addConnection(signal_index, c.get());
    locker.unlock();
    QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index);
    if (smethod.isValid())
        s->connectNotify(smethod);
    return c.release();
}
void QObjectPrivate::addConnection(int signal, Connection *c)
{
    Q_ASSERT(c->sender == q_ptr);
    ensureConnectionData();
    ConnectionData *cd = connections.loadRelaxed();
    cd->resizeSignalVector(signal + 1);
    //取得链接列表list
    ConnectionList &connectionList = cd->connectionsForSignal(signal);
    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();
    //【4】注意:senders是 Connection *
    // 这里操作元对象中的Connections
    c->prev = &(rd->connections.loadRelaxed()->senders);
    c->next = *c->prev;
    *c->prev = c;
    if (c->next)
        c->next->prev = &c->next;
}

【4】Connection

    struct Connection
    {
        //发送者
        QObject *sender;
        //接收者
        QObject *receiver;
        union {
            //接收者static_meatcall函数地址
            StaticMetaCallFunction callFunction;
            QtPrivate::QSlotObjectBase *slotObj;
        };
        // The next pointer for the singly-linked ConnectionList
        Connection *nextConnectionList;
        //senders linked list
        Connection *next;
        Connection **prev;
        QAtomicPointer<const int> argumentTypes;
        QAtomicInt ref_;
        ushort method_offset;
        ushort method_relative;
        uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
        ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
        ushort isSlotObject : 1;
        ushort ownArgumentTypes : 1;
        Connection() : nextConnectionList(nullptr), ref_(2), ownArgumentTypes(true) {
            //ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
        }
        ~Connection();
        int method() const { Q_ASSERT(!isSlotObject); return method_offset + method_relative; }
        void ref() { ref_.ref(); }
        void deref() {
            if (!ref_.deref()) {
                Q_ASSERT(!receiver);
                delete this;
            }
        }
    };
    // ConnectionList is a singly-linked list
    struct ConnectionList {
        ConnectionList() : first(nullptr), last(nullptr) {}
        Connection *first;
        Connection *last;
    };

槽函数调用过程

虚伪的emit

发射信号时,我们常使用 emit,但它是一个空的宏:

# define emit

也就是说,我们 emit signal() 和 直接调用 signal() 完全没有区别。

源码分析

那么我们 emit user_signal() 就变成了直接调用 user_signal()user_signal() 只调用了QMetaObject::activate(this, &staticMetaObject, 0, nullptr);,跟踪下源码:

void QMetaObject::activate(QObject *sender, int signal_index, void **argv)
{
    const QMetaObject *mo = sender->metaObject();
    //若信号非子类实现,则可能是其祖先类信号,遍历找到血缘关系最近且具有该信号的类
    while (mo->methodOffset() > signal_index)
        mo = mo->superClass();
    activate(sender, mo, signal_index - mo->methodOffset(), argv);
}

void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;
    if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
        doActivate<true>(sender, signal_index, argv);
    else
        doActivate<false>(sender, signal_index, argv);
 }

template <bool callbacks_enabled>
 void doActivate(QObject *sender, int signal_index, void **argv)
{
    //...
    //链接记录列表
    const QObjectPrivate::ConnectionList *list;
    if (signal_index < signalVector->count())
        list = &signalVector->at(signal_index);
    else
        list = &signalVector->at(-1);
    //获取当前线程id
    Qt::HANDLE currentThreadId = QThread::currentThreadId();
    //判断是否为发射者线程id
    bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData->threadId.loadRelaxed();
    // We need to check against the highest connection id to ensure that signals added
    // during the signal emission are not emitted in this emission.
    uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
    //遍历连接
    do {
        QObjectPrivate::Connection *c = list->first.loadRelaxed();
        if (!c)
            continue;
        //遍历槽
        do {
            QObject * const receiver = c->receiver.loadRelaxed();
            if (!receiver)
                continue;
            QThreadData *td = c->receiverThreadData.loadRelaxed();
            if (!td)
                continue;
            //判断是否处于同一线程
            bool receiverInSameThread;
            if (inSenderThread) {
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            } else {
                // need to lock before reading the threadId, because moveToThread() could interfere
                QMutexLocker lock(signalSlotLock(receiver));
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            }
            // determine if this connection should be sent immediately or
            // put into the event queue
            // 队列连接或自动连接且不在同一线程中则放入发射队列
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                //【1】
                queued_activate(sender, signal_index, c, argv);
                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;
                {
                    QBasicMutexLocker locker(signalSlotLock(sender));
                    if (!c->receiver.loadAcquire())
                        continue;
                    QMetaCallEvent *ev = c->isSlotObject ?
                        new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv, &semaphore) :
                        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv, &semaphore);
                    // 发送事件-槽函数由事件系统调用
                    //【2】
                    QCoreApplication::postEvent(receiver, ev);
                }
                semaphore.acquire();
                continue;
#endif
            }
            //直接调用槽函数或回调函数
            //【3】
            QObjectPrivate::Sender senderData(receiverInSameThread ? receiver : nullptr, sender, signal_index);
            if (c->isSlotObject) {
                c->slotObj->ref();
                struct Deleter {
                    void operator()(QtPrivate::QSlotObjectBase *slot) const {
                        if (slot) slot->destroyIfLastRef();
                    }
                };
                const std::unique_ptr<QtPrivate::QSlotObjectBase, Deleter> obj{c->slotObj};
                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.get());
                    obj->call(receiver, argv);
                }
            } 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 method_relative = c->method_relative;
                const auto callFunction = c->callFunction;
                const int methodIndex = (Q_HAS_TRACEPOINTS || callbacks_enabled) ? c->method() : 0;
                if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr)
                    signal_spy_set->slot_begin_callback(receiver, methodIndex, argv);
                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
                    //调用回调函数
                    callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
                }
                if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                    signal_spy_set->slot_end_callback(receiver, methodIndex);
            } else {
                const int method = c->method_relative + c->method_offset;
                if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr) {
                    signal_spy_set->slot_begin_callback(receiver, method, argv);
                }
                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
                    //直接调用(元对象系统实现反射)
                    QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
                }
                if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                    signal_spy_set->slot_end_callback(receiver, method);
            }
        } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
    } while (list != &signalVector->at(-1) &&
        //start over for all signals;
        ((list = &signalVector->at(-1)), true));
        if (connections->currentConnectionId.loadRelaxed() == 0)
            senderDeleted = true;
    }
    if (!senderDeleted)
        sp->connections.loadRelaxed()->cleanOrphanedConnections(sender);
    if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr)
        signal_spy_set->signal_end_callback(sender, signal_index);
}

加了注释以下若还是觉得不够清晰,可以查看以下缩略版 doActivate()

template <bool callbacks_enabled>
 void doActivate(QObject *sender, int signal_index, void **argv)
{
    //...

    //获取当前线程id
    Qt::HANDLE currentThreadId = QThread::currentThreadId();
    //判断是否为发射者线程id
    bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData->threadId.loadRelaxed();
    // We need to check against the highest connection id to ensure that signals added
    // during the signal emission are not emitted in this emission.
    uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
    //遍历连接
    do {
        //...
        //遍历槽
        do {
            //...
            //判断是否处于同一线程
            bool receiverInSameThread;
            if (inSenderThread) {
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            } else {
                // need to lock before reading the threadId, because moveToThread() could interfere
                QMutexLocker lock(signalSlotLock(receiver));
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            }
            // 队列连接或自动连接且不在同一线程中则放入发射队列
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                    //...
#if QT_CONFIG(thread)
            // 阻塞连接
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
                // ...
#endif
            }
            //其他:直接调用槽函数或回调函数

        } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
    } while (list != &signalVector->at(-1) &&
        //start over for all signals;
        ((list = &signalVector->at(-1)), true));
}

可以发现:

  • 同一个线程内的信号-槽,就相当于函数调用,和前面的观察者模式相似。

  • 跨线程的信号-槽,在信号触发时,发送者线程将槽函数的调用转化成了一次“调用事件”,放入事件循环中。接收者线程执行到下一次事件处理时,处理“调用事件”,调用相应的函数。

槽函数调用过程总结

emit user_signal() 经历了什么:

虚伪的emit
emit signal
signal函数
QMetaObject::activate
doActivate

首先 emit 将被脱去,直接调用 user_signal()user_signal() 的实现是 moc 生成的。

它只调用了QMetaObject::activate(QObject *sender, int signal_index, void **argv),在这里如果若信号非子类实现,则可能是其祖先类信号,遍历找到血缘关系最近且具有该信号的类,再调用 QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)

最后殊途同归,调用 doActivate(QObject *sender, int signal_index, void **argv),该函数将完成槽函数的调用。

Qt - 一文理解信号槽机制(万字剖析整理)_第5张图片

参考鸣谢

Qt实用技能4-认清信号槽的本质

Qt原理-窥探信号槽的实现细节

QT 的信号与槽机制介绍

Qt connect信号连接的几种写法

菜鸟教程-观察者模式(笔记区更精彩)

简说设计模式——观察者模式

观察者模式(Observer模式)详解

Qt信号槽-原理分析

Qt5学习应用之路–信号与槽(signal & slot)

深入理解QT的SIGNAL\SLOT机制(二):QMetaObject相关知识

深入理解QT的SIGNAL\SLOT机制(三):QObject::connect函数

深入理解QT的SIGNAL\SLOT机制(四):Connection结构

深入理解QT的SIGNAL\SLOT机制(五):信号的发射过程

你可能感兴趣的:(Qt,信号槽,connect,moc元对象编辑器,对象间通讯机制,源码分析)