首页 Qt信号槽机制梳理
文章
取消

Qt信号槽机制梳理

参考链接:Qt信号槽-原理分析 - 朝十晚八 - 博客园 (cnblogs.com)

Qt的信号/槽机制是一种事件驱动机制,它在某种程度上类似于C++的函数指针,但它更为强大和灵活。信号/槽机制的工作原理如下:

  1. 信号(Signal):当在程序中发生某个事件时,一个信号被发出。例如,当用户点击一个按钮时,按钮对象可能会发出一个”被点击”的信号。
  2. 槽(Slot):槽是一个函数,它是在信号被发出时调用的。一个槽可以连接一个或多个信号,当任何一个连接的信号被发出时,槽都会被调用。
  3. 连接(Connect):信号和槽之间的连接是通过QObject::connect()函数建立的。当一个信号被发出时,所有连接到该信号的槽都会被调用。

Qt的信号/槽机制是如何实现的呢?其背后的机制包括元对象系统(Meta-Object System,MOS)和事件队列。

  • 元对象系统:Qt通过MOC(Meta-Object Compiler,元对象编译器)扩展了C++的语言特性。MOC会读取包含Q_OBJECT宏的头文件,并生成一个相应的C++源文件,这个源文件包含了类的元信息,如类名、父类名、信号/槽的名字等。这就是Qt能够在运行时进行类型检查和信号与槽的匹配的原因。
  • 事件队列:当一个信号被发出时,如果连接到该信号的槽函数没有立即执行,那么这个调用就会被放入到事件队列中,等待事件循环下一次循环时执行。这意味着,即使在多线程环境中,槽函数也总是在它所属的线程中被执行,这大大简化了线程同步的问题。

下面进一步深入。

moc系统

首先,给定一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass : public QObject
{
    Q_OBJECT // 这个宏告诉MOC这个类要使用元对象系统

public:
    MyClass(QObject *parent = nullptr);

signals:
    void mySignal(int value); // 定义一个带有一个int参数的信号

public slots:
    void mySlot(int value); // 定义一个带有一个int参数的槽
};

Q_OBJECT 这个宏是Qt元对象的核心,查看它的展开如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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, "")

相当于对于需要Qt信号槽机制的类,比如注册Qt的元对象(即Q_OBJECT),然后Q_OBJECT为这个类添加了一些列元对象结构和相应的函数接口,比如上面的核心staticMetaObjectqt_static_metacall .

查看该头文件编译后的moc_xx.cpp:

  • moc_xx.cpp源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    
      QT_BEGIN_MOC_NAMESPACE
      QT_WARNING_PUSH
      QT_WARNING_DISABLE_DEPRECATED
      struct qt_meta_stringdata_MyWidget_t {
          QByteArrayData data[4];
          char stringdata0[26];
      };
      #define QT_MOC_LITERAL(idx, ofs, len) \
          Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
          qptrdiff(offsetof(qt_meta_stringdata_MyWidget_t, stringdata0) + ofs \
              - idx * sizeof(QByteArrayData)) \
          )
      static const qt_meta_stringdata_MyWidget_t qt_meta_stringdata_MyWidget = {
          {
      QT_MOC_LITERAL(0, 0, 8), // "MyWidget"
      QT_MOC_LITERAL(1, 9, 8), // "mySignal"
      QT_MOC_LITERAL(2, 18, 0), // ""
      QT_MOC_LITERAL(3, 19, 6) // "mySlot"
        
          },
          "MyWidget\0mySignal\0\0mySlot"
      };
      #undef QT_MOC_LITERAL
        
      static const uint qt_meta_data_MyWidget[] = {
        
       // content:
             8,       // revision
             0,       // classname
             0,    0, // classinfo
             2,   14, // methods
             0,    0, // properties
             0,    0, // enums/sets
             0,    0, // constructors
             0,       // flags
             1,       // signalCount
        
       // signals: name, argc, parameters, tag, flags
             1,    0,   24,    2, 0x06 /* Public */,
        
       // slots: name, argc, parameters, tag, flags
             3,    0,   25,    2, 0x0a /* Public */,
        
       // signals: parameters
          QMetaType::Void,
        
       // slots: parameters
          QMetaType::Void,
        
             0        // eod
      };
        
      void MyWidget::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
      {
          if (_c == QMetaObject::InvokeMetaMethod) {
              auto *_t = static_cast<MyWidget *>(_o);
              Q_UNUSED(_t)
              switch (_id) {
              case 0: _t->mySignal(); break;
              case 1: _t->mySlot(); break;
              default: ;
              }
          } else if (_c == QMetaObject::IndexOfMethod) {
              int *result = reinterpret_cast<int *>(_a[0]);
              {
                  using _t = void (MyWidget::*)();
                  if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MyWidget::mySignal)) {
                      *result = 0;
                      return;
                  }
              }
          }
          Q_UNUSED(_a);
      }
        
      QT_INIT_METAOBJECT const QMetaObject MyWidget::staticMetaObject = { {
          &QWidget::staticMetaObject,
          qt_meta_stringdata_MyWidget.data,
          qt_meta_data_MyWidget,
          qt_static_metacall,
          nullptr,
          nullptr
      } };
        
      const QMetaObject *MyWidget::metaObject() const
      {
          return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
      }
        
      void *MyWidget::qt_metacast(const char *_clname)
      {
          if (!_clname) return nullptr;
          if (!strcmp(_clname, qt_meta_stringdata_MyWidget.stringdata0))
              return static_cast<void*>(this);
          return QWidget::qt_metacast(_clname);
      }
        
      int MyWidget::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
      {
          _id = QWidget::qt_metacall(_c, _id, _a);
          if (_id < 0)
              return _id;
          if (_c == QMetaObject::InvokeMetaMethod) {
              if (_id < 2)
                  qt_static_metacall(this, _c, _id, _a);
              _id -= 2;
          } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
              if (_id < 2)
                  *reinterpret_cast<int*>(_a[0]) = -1;
              _id -= 2;
          }
          return _id;
      }
        
      // SIGNAL 0
      void MyWidget::mySignal()
      {
          QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
      }
      QT_WARNING_POP
      QT_END_MOC_NAMESPACE
    

结合其中Qt生成的注释来理解:Moc编译器创建了一个包含信号和槽信息的元对象。这个元对象是MyWidget类的一部分,并包含了类名、信号和槽的名称、信号和槽的参数类型等信息。

元对象的结构

元对象的定义部分看起来是这样的:

1
2
3
4
5
6
7
8
QT_INIT_METAOBJECT const QMetaObject MyWidget::staticMetaObject = { {
    &QWidget::staticMetaObject, // 父类的元对象
    qt_meta_stringdata_MyWidget.data, // 类名、信号和槽的名称
    qt_meta_data_MyWidget, // 其他元数据,包括信号和槽的参数类型
    qt_static_metacall, // 用于调用信号和槽的函数
    nullptr,// 属性列表
    nullptr// 枚举列表
} };

可以看到,元对象包含了一些元数据,如类名、信号和槽的名称等。这些信息是在编译时由Moc编译器生成的,并存储在qt_meta_stringdata_MyWidgetqt_meta_data_MyWidget这两个静态变量中。

信号和槽的调用

元对象还包含一个qt_static_metacall函数,这个函数是用来调用信号和槽的。这个函数会接收一个指向QObject的指针、一个表示调用类型的枚举值(如InvokeMetaMethod)、一个表示被调用方法的索引以及一个参数列表。

在这个例子中,qt_static_metacall函数的实现是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MyWidget::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<MyWidget *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->mySignal(); break; // 如果_id为0,表示调用mySignal信号
        case 1: _t->mySlot(); break; // 如果_id为1,表示调用mySlot槽
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        // ...
    }
    Q_UNUSED(_a);
}

信号槽调用

当Qt需要发射一个信号或调用一个槽时,它会调用qt_static_metacall函数,并传递相应的参数。例如,如果代码中有这样一行:

1
emit obj.mySignal();

查看emit,可以看到:

1
# define emit

emit关键字在C++中本身并不具有任何意义,它只是一个标记,用于在语义上表示发出一个信号。实际上,emit在预处理后会被完全忽略。当你写下 emit someSignal(); 时,实际上等同于直接调用 someSignal();

emit关键字在C++中本身并不具有任何意义,它只是一个标记,用于在语义上表示发出一个信号。实际上,emit在预处理后会被完全忽略。当你写下 emit someSignal(); 时,实际上等同于直接调用 someSignal();

那么,信号函数是如何实现的呢?在Qt中,信号是通过QMetaObject::activate函数进行发射的。下面是你提供的mySignal信号函数的实现:

1
2
3
4
void MyWidget::mySignal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

QMetaObject::activate函数接受四个参数:发射信号的对象、对象的元对象、信号在元对象中的索引以及信号的参数列表。在这个例子中,发射信号的对象是this,元对象是staticMetaObject,信号在元对象中的索引是0,信号没有参数,所以参数列表是nullptr

QMetaObject::activate函数的工作原理是这样的:

  1. 首先,它会获取信号的接收者列表。这个列表是在信号和槽连接时创建的,包含了所有连接到这个信号的槽。
  2. 然后,它会遍历这个列表,对每一个接收者,都会调用其对应的槽。这是通过调用接收者的qt_metacall函数实现的。
  3. qt_metacall函数会根据传入的索引调用相应的槽。这个过程和我之前描述的qt_static_metacall函数的工作方式类似。唯一的区别是qt_metacall函数是虚函数,可以在子类中被重写,而qt_static_metacall函数是静态函数。

connect

以下部分没认真查看源码,主要来源参考GPT和上面参考链接

在进一步之前,我们来看connect的实现:

QObject::connect 函数是 Qt 信号/槽机制的核心,它用于将信号和槽连接起来。在 Qt 中,你可以这样使用 connect 函数:

1
QObject::connect(sender, SIGNAL(someSignal()), receiver, SLOT(someSlot()));

sender 发射 someSignal 信号时,receiversomeSlot 槽就会被调用。

相应的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;//是否是槽对象 默认是true
c->argumentTypes.store(types);//参数类型
c->nextConnectionList = 0;//指向下个连接对象
c->callFunction = callFunction;//静态回调函数,也就是qt_static_metacall

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

connect干的事情并不多,好像就是构造了一个Connection对象,然后存储在了发送者的内存中。在Qt中,QObject::connect函数创建的槽连接会保存在发射信号的QObject实例中。具体来说,每个QObject实例都有一个QObjectPrivate的私有数据成员。这个QObjectPrivate中有一个名为connectionLists的成员,它就是用来保存槽连接的。每一个槽连接包含了接收者、槽的索引以及其他的一些信息。

信号槽连接后在内存中已QObjectConnectionListVector对象存储,这是一个数组,Qt巧妙的借用了数组快速访问指定元素的方式,把信号所在的索引作为下标来索引他连接的Connection对象,众所周知一个信号可以被多个槽连接,那么我们的的数组自然而然也就存储了一个链表,用于方便的插入和移除,也就是CommectionList对象。

1
class QObjectConnectionListVector : public QVector<QObjectPrivate::ConnectionList>

QObject::connect函数被调用时,它会创建一个新的槽连接,并将其添加到connectionLists中。而当QMetaObject::activate函数被调用时,它会遍历connectionLists,并对每一个槽连接调用相应的槽。

信号触发

Qt为我们提供了5种类型的连接方式,如下

  • Qt::AutoConnection 自动连接,根据sender和receiver是否在一个线程里来决定使用哪种连接方式,同一个线程使用直连,否则使用队列连接
  • Qt::DirectConnection 直连
  • Qt::QueuedConnection 队列连接
  • Qt::BlockingQueuedConnection 阻塞队列连接,顾名思义,虽然是跨线程的,但是还是希望槽执行完之后,才能执行信号的下一步代码
  • Qt::UniqueConnection 唯一连接

一般情况下,我们都使用默认的连接方式,除非一些特殊的需求,我们才会主动指定连接方式。当我们执行信号时,函数的调用关系可能会像下面这样

img

  1. 直连

对于大多数的开发工作来说,我们可能都是在同一个线程里进行的,因此直连也是我们使用连接方式最多的一种,直连说白了就是函数回调。还记得我们第三小节讲的connect吗,他构造了一个Connection对象,存储在了发送者的内存中,直连其实就是调用了我们之前存储在Connection中的函数地址。以上代码调用的堆栈(没有加上Qt源码部分的堆栈信息):

img

  1. 队列连接

connect连接信号槽时,我们使用Qt::QueuedConnection作为连接类型时,槽函数的执行是通过抛出QMetaCallEvent事件,经过Qt的事件循环达到异步的效果。

下面代码摘自Qt源码,queued_activate函数即是处理队列请求的函数,当我们使用自动连接并且接受者和发送者不在一个线程时使用队列连接;或者当我们指定连接方式为队列时使用队列连接。

1
2
3
4
5
6
// determine if this connection should be sent immediately or
// put into the event queue
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
    || (c->connectionType == Qt::QueuedConnection)) {
    queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker);
    continue;
  • 测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
      class MyWidget : public QObject {
      Q_OBJECT
      public:
          explicit MyWidget(QObject *parent = nullptr) : QObject(parent) {}
        
      public slots:
        
          void mySlot() {
              qDebug() << "mySlot";
              // 暂停1秒
              QThread::sleep(10);
          }
        
      };
        
      class MyWidget2 : public QWidget {
      Q_OBJECT
      public:
          explicit MyWidget2(QWidget *parent = nullptr) : QWidget(parent) {
              auto myWidget = new MyWidget();
        
              QThread *thread = new QThread();
              myWidget->moveToThread(thread);
        
              connect(this, &MyWidget2::mySignal, myWidget, &MyWidget::mySlot, Qt::QueuedConnection);
        
              thread->start();
        
              auto timer = new QTimer(this);
              connect(timer, &QTimer::timeout, this, [this]() {
                  emit mySignal();
              });
              timer->start(1000);
        
              auto timer2 = new QTimer(this);
              connect(timer2, &QTimer::timeout, this, [this]() {
                  qDebug() << "continue...";
              });
              timer2->start(1000);
          }
        
      signals:
        
          void mySignal();
      };
    

查看调用栈:

img

总结

使用 QObject::connect 绑定信号槽的执行工作原理:

  1. 首先,connect 函数会通过信号和槽的名字查找它们在元对象中的索引。这是通过调用 QMetaObject::indexOfSignalQMetaObject::indexOfMethod 函数实现的。
  2. 然后,connect 函数会创建一个槽连接,保存了信号发射者、信号的索引、接收者以及槽的索引等信息。这个槽连接会被添加到发射者的一个列表中,这个列表保存了该发射者的所有槽连接。
  3. 当发射者发射信号时,会调用 QMetaObject::activate 函数。activate 函数会遍历发射者的槽连接列表,找到所有与该信号相关的槽连接。
  4. 对于每一个槽连接,activate 函数会检查接收者是否还存在(因为接收者可能已经被删除)。如果接收者存在,就会调用接收者的 qt_metacall 函数,传入槽的索引和信号的参数。
  5. 最后,qt_metacall 函数会根据传入的索引调用相应的槽
本文由作者按照 CC BY 4.0 进行授权

QObject体系对象数据系统

南方基金年报