分享

Qt的slot和singnal的本质

 懒人海马 2023-02-27 发布于山东

 Qt的slot的signal是一种对象之间的通信方式,在讲这个之前,要讲一下Qt的元对象系统


Qt的元对象系统 Meta Object System

Qt的元对象系统 Meta Object System 主要是分成这个几个部分

(1)所有的类都是QObject的的子类

(2)Q_OBJECT的宏定义会激活元对象的功能,例如说信号和槽

(3)如果定义了 Q_OBJECT  宏定义,会让moc编辑器自动生成一些代码和类来支持元对象的功能。

至少是让该类产生了这么几个数据

    static const QMetaObject  staticMetaObject;     virtual const QMetaObject *metaObject() const;     virtual void *qt_metacast(const char *);     virtual int qt_metacall(QMetaObject::Call, int, void **); 
    static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **)(Q5.12.4)

      (1)其中QMetaObject静态变量staticMetaObject包含了QObject的所谓的元数据,也就是QObject信息的一些描述信息:除了类型信息外,还包含QT中特有的signal&slot信息。这些信息是整个类共享的,所用用static来申明。                

  1. struct Q_CORE_EXPORT QMetaObject
  2. {
  3. // ......
  4. struct { // private data
  5. const QMetaObject *superdata;
  6. const char *stringdata;
  7. const uint *data;
  8. const void *extradata;
  9. } d;
  10. };

       上面是QMetaObject的申明,就是这些数据成员记录了所有的signal、slot、property、class information信息。

  •       const QMetaObject *superdata,该变量指向与之对应的QObject类的父类对象,或者是祖先类的QMetaObject对象。每一个QObject类或者其派生类可能有一个父类或者父类的父类。那么superdata就是指向与其最接近的祖先类中的QMetaObject对象。如果没有父类,那么该变量就是一个NULL指针。
  • const char*stringdata,这是一个指向string data的指针。但它和我们平时所使用的一般的字符串指针却很不一样,我们平时使用的字符串指针只是指向一个字符串的指针,而这个指针却指向的是很多个字符串。那么它不就是字符串数组吗?哈哈,也不是。因为C++的字符串数组要求数组中的每一个字符串拥有相同的长度,这样才能组成一个数组。那它是不是一个字符串指针数组呢?也不是,那它到底是什么呢?让我们来看一看它的具体值,以QObject这个class的QMetaObject为例来说明。
    1. static const char qt_meta_stringdata_QObject[] =
    2. {
    3. "QObject\0\0destroyed(QObject*)\0destroyed()\0"
    4. "deleteLater()\0_q_reregisterTimers(void*)\0"
    5. "QString\0objectName\0parent\0QObject(QObject*)\0"
    6. "QObject()\0"
    7. };

    这个字符串都是些什么内容呀?有,Class Name, Signal Name,Slot Name, Property Name。看到这些大家是不是觉得很熟悉呀,对啦,他们就是MetaSystem所支持的最核心的功能属性了。

  • const unit *data,这个指针指向一个正整数数组,只不过在不同的object中数据的长度不一定相同,这取决于与之相应的class中定义了多少signal、slot和property。

    这个整数数组的值,有一部分指出了前一个变量(stringdata)中不同字符串的索引值,但是需要注意的是,这里面的数值并不是直接标明了每一个字符串的索引值,这个数组还需要通过一个相应的算法计算后,才能获得正确的字符串的索引值。

    1. static const uint qt_meta_data_QObject[] =
    2. {
    3. //content:
    4. 2, //revision
    5. 0, //classname
    6. 0, 0, //classinfo
    7. 4, 12, //methods
    8. 1, 32, //properties
    9. 0, 0, //enums/sets
    10. 2, 35, //constructors
    11. //signals:signature,parameters,type,tag,flags
    12. 9, 8, 8, 8, 0x05,
    13. 29, 8, ,8 8, 0x25,
    14. //slots:signature,parameters,type,tag,flags
    15. 41, 8, 8, 8, 0x0a,
    16. 55, 8, 8, 8, 0x08,
    17. //properties:name,type,flags
    18. 90, 82, 0x0a095103,
    19. //constructors:signature,parameters,type,tag,flags
    20. 108, 101, 8, 8, 0x0e,
    21. 126, 8, 8, 8, 0x2e,
    22. 0 //end
    23. };

    第一个section,就是 //content 区域的整数值,这一块区域在每一个QMetaObject的实体对象中数量都是相同的,含义也相同,但具体的值就不同了。专门有一个struct定义了这个section,其含义在上面的注释中已经说的很清楚了。第二个section,以 // signals 开头的这段。这个section中的数值指明了QObject这个class包含了两个signal,第三个section,以 // slots 开头的这段。这个section中的数值指明了QObject这个class包含了两个slot。第四个section,以 // properties 开头的这段。这个section中的数值指明了QObject这个class包含有一个属性定义。第五个section,以 // constructors 开头的这段,指明了QObject这个class有两个constructor。

     (2)   virtual QObject::metaObject(); 这个的方法是返回一个该类对应的staticMetaObject的const 指针。也就是引用。

如果说这个类没有定义Q_OBJECT 宏定义,那么这个类就没有staticMetaObject这个静态的QMetaObject对象,那么这个时候这实例调用返回是是其父类的QMetaObject对象,因为这个虚函数没有被重载。所以一般建议都加上这个宏定义。

      (3)qt_static_metacall()是一个静态函数,主要是用于对象的函数的调用的。Qt 5.12版本或少以上,这个静态函数也会通过指针的方式存储在QMetaObject 里面了。qt_static_metacall()提供了两种的函数调用的方式,一个是InvokeMetaMethod调用,会将对象转换为实际的对象类型,然后调用。另外一个是IndexOfMethod调用,会通过元函数的索引号调用。

 


Qt的slot和singnal

qt的slot和singnal的名称都会存在的QMetaObject中,但是这个时候,slot和signal是没有联系的。需要进行connect才可以使用。

 例如:connect(ui->pushBtn,SIGNAL(clicked()),ui->lineEdit,SLOT(clear()));

 connect 在幕后到底都做了些什么事情?为什么emit一个signal后,相应的slot都会被调用?

SIGNAL和SLOT宏定义

connect(&obj, SIGNAL(destroyed()), &app, SLOT(aboutQt()));

在这里signal和slot的名字都被包含在了两个大写的SIGNAL和SLOT中,这两个是什么呢?原来SIGNAL 和 SLOT 是Qt定义的两个宏。

# define SLOT(a)      ”1″#a  
# define SIGNAL(a)   ”2″#a  

Qt把signal和slot都转化成了字符串,并且还在这个字符串的前面加上了附加的符号,signal前面加了’2’,slot前面加了’1’。也就是说,我们前面写了下面的connect调用,在经过moc编译器转换之后,就便成了:
connect(&obj, “2destroyed()”, &app, “1aboutQt()”)); 当connect函数被调用了之后,都会去检查这两个参数是否是使用这两个宏正确的转换而来的,它检查的根据就是这两个前置数字,是否等于1或者是2,如果不是,connect函数当然就会失败啦!

 然后,会去检查发送signal的对象是否有这个signal,方法就是查找这个对象的class所对应的staticMetaObject对象中所包含的d.stringdata所指向的字符串中是否包含这个signal的名字,在这个检查过程中,就会用到d.data所指向的那一串整数,通过这些整数值来计算每一个具体字符串的起始地址。同理,还会使用同样的方法去检查slot,看响应这个signal的对象是否包含有相应的slot。这两个检查的任何一个如果失败的话,connect函数就失败了,返回false。

前面的步骤都是在做一些必要的检查工作,下一步,就是要把发送signal的对象和响应signal的对象关联起来。在QObject的私有数据类QObjectPrivate中,有下面这些数据结构来保存这些信息:

  1. class QObjectPrivate : public QObjectData
  2. {
  3.   struct Connection
  4.   {
  5.     QObject *receiver;
  6.     int method;
  7.     uint connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
  8.     QBasicAtomicPointer<int> argumentTypes;
  9.   };
  10.   typedef QList<Connection>; ConnectionList;
  11.   QObjectConnectionListVector *connectionLists;
  12.   struct Sender
  13.   {
  14.     QObject *sender;
  15.     int signal;
  16.     int ref;
  17.   };
  18.   QList<Sender> senders;
  19. };

    在发送signal的对象中,每一个signal和slot的connection,都会创建一个QObjectPrivate::Connection对象,并且把这个对象保存到connectionList这个Vector里面去。

   在响应signal的对象中,同样,也是每一个signal和slot的connection,都会一个创建一个Sender对象,并且把这个对象附加在Senders这个列表中。

 

emit幕后的故事

当emit signal的时候,与这个signal相连的slot函数就会被调用,那么这个调用时如何发生的呢?

看段代码:

  1. class ZMytestObj : public QObject
  2. {
  3.   Q_OBJECT
  4.   signals:
  5.   void sigMenuClicked();
  6.   void sigBtnClicked();
  7. };
  1. void ZMytestObj::sigMenuClicked()
  2. {
  3.   QMetaObject::activate(this,&staticMetaObject,0,0);
  4. }
  5. void ZMytestObj::sigBtnClicked()
  6. {
  7.   QMetaObject::activate(this,&staticMetaObject,1,0);
  8. }

   每一个signal都会被转换为一个与之相对应的成员函数。也就是说,当我们写下这样一行代码:emit sigBtnClicked();当程序运行到这里的时候,实际上就是调用了void ZMytestObj::sigBtnClicked() 这个函数。

  void ZMytestObj::sigMenuClicked()  void ZMytestObj::sigBtnClicked(),它们唯一的区别就是调用 QMetaObject::activate 函数时给出的参数不同,一个是0,一个是1,它们的含义是什么呢?它们表示是这个类中的第几个signal被发送出来了,回头再去看头文件就会发现它们就 是在这个类定义中,signal定义出现的顺序,这个参数可是非常重要的,它直接决定了进入这个函数体之后所发生的事情。

当执行流程进入到QMetaObject::activate函数中后,会先从connectionLists这个变量中取出与这个signal相对应的connection list,它根据的就是刚才所传入进来的signal index。这个connection list中保存了所有和这个signal相链接的slot的信息,每一对connection(即:signal和slot的连接)是这个list中的一项。

在每个一具体的链接记录中,还保存了这个链接的类型,是自动链接类型,还是队列链接类型,或者是阻塞链接类型,不同的类型处理方法还不一样的。

  1. if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
  2. || (c->connectionType == Qt::QueuedConnection)) {
  3. // 队列处理
  4. } else if (c->connectionType == Qt::BlockingQueuedConnection) {
  5. // 阻塞处理
  6. // 如果同线程,打印潜在死锁。
  7. } else {
  8. //直接调用槽函数或回调函数
  9. }

    如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。

  如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)

   队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent

   放进了事件循环, 等到下一次事件分发,相应的线程才会去调用槽函数。

    对于直接链接的类型,先找到接收这个signal的对象的指针,然后是处理这个signal的slot的index,已经是否有需要处理的参数,然后就使用这些信息去调用receiver的qt_metcall 方法。在qt_metcall方法中就简单了,根据slot的index,一个大switch语句,调用相应的slot函数就OK了。 Qt的slot的signal是一种对象之间的通信方式,在讲这个之前,要讲一下Qt的元对象系统


Qt的元对象系统 Meta Object System

Qt的元对象系统 Meta Object System 主要是分成这个几个部分

(1)所有的类都是QObject的的子类

(2)Q_OBJECT的宏定义会激活元对象的功能,例如说信号和槽

(3)如果定义了 Q_OBJECT  宏定义,会让moc编辑器自动生成一些代码和类来支持元对象的功能。

至少是让该类产生了这么几个数据

    static const QMetaObject  staticMetaObject;     virtual const QMetaObject *metaObject() const;     virtual void *qt_metacast(const char *);     virtual int qt_metacall(QMetaObject::Call, int, void **); 
    static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **)(Q5.12.4)

      (1)其中QMetaObject静态变量staticMetaObject包含了QObject的所谓的元数据,也就是QObject信息的一些描述信息:除了类型信息外,还包含QT中特有的signal&slot信息。这些信息是整个类共享的,所用用static来申明。                

  1. struct Q_CORE_EXPORT QMetaObject
  2. {
  3. // ......
  4. struct { // private data
  5. const QMetaObject *superdata;
  6. const char *stringdata;
  7. const uint *data;
  8. const void *extradata;
  9. } d;
  10. };

       上面是QMetaObject的申明,就是这些数据成员记录了所有的signal、slot、property、class information信息。

  •       const QMetaObject *superdata,该变量指向与之对应的QObject类的父类对象,或者是祖先类的QMetaObject对象。每一个QObject类或者其派生类可能有一个父类或者父类的父类。那么superdata就是指向与其最接近的祖先类中的QMetaObject对象。如果没有父类,那么该变量就是一个NULL指针。
  • const char*stringdata,这是一个指向string data的指针。但它和我们平时所使用的一般的字符串指针却很不一样,我们平时使用的字符串指针只是指向一个字符串的指针,而这个指针却指向的是很多个字符串。那么它不就是字符串数组吗?哈哈,也不是。因为C++的字符串数组要求数组中的每一个字符串拥有相同的长度,这样才能组成一个数组。那它是不是一个字符串指针数组呢?也不是,那它到底是什么呢?让我们来看一看它的具体值,以QObject这个class的QMetaObject为例来说明。
    1. static const char qt_meta_stringdata_QObject[] =
    2. {
    3. "QObject\0\0destroyed(QObject*)\0destroyed()\0"
    4. "deleteLater()\0_q_reregisterTimers(void*)\0"
    5. "QString\0objectName\0parent\0QObject(QObject*)\0"
    6. "QObject()\0"
    7. };

    这个字符串都是些什么内容呀?有,Class Name, Signal Name,Slot Name, Property Name。看到这些大家是不是觉得很熟悉呀,对啦,他们就是MetaSystem所支持的最核心的功能属性了。

  • const unit *data,这个指针指向一个正整数数组,只不过在不同的object中数据的长度不一定相同,这取决于与之相应的class中定义了多少signal、slot和property。

    这个整数数组的值,有一部分指出了前一个变量(stringdata)中不同字符串的索引值,但是需要注意的是,这里面的数值并不是直接标明了每一个字符串的索引值,这个数组还需要通过一个相应的算法计算后,才能获得正确的字符串的索引值。

    1. static const uint qt_meta_data_QObject[] =
    2. {
    3. //content:
    4. 2, //revision
    5. 0, //classname
    6. 0, 0, //classinfo
    7. 4, 12, //methods
    8. 1, 32, //properties
    9. 0, 0, //enums/sets
    10. 2, 35, //constructors
    11. //signals:signature,parameters,type,tag,flags
    12. 9, 8, 8, 8, 0x05,
    13. 29, 8, ,8 8, 0x25,
    14. //slots:signature,parameters,type,tag,flags
    15. 41, 8, 8, 8, 0x0a,
    16. 55, 8, 8, 8, 0x08,
    17. //properties:name,type,flags
    18. 90, 82, 0x0a095103,
    19. //constructors:signature,parameters,type,tag,flags
    20. 108, 101, 8, 8, 0x0e,
    21. 126, 8, 8, 8, 0x2e,
    22. 0 //end
    23. };

    第一个section,就是 //content 区域的整数值,这一块区域在每一个QMetaObject的实体对象中数量都是相同的,含义也相同,但具体的值就不同了。专门有一个struct定义了这个section,其含义在上面的注释中已经说的很清楚了。第二个section,以 // signals 开头的这段。这个section中的数值指明了QObject这个class包含了两个signal,第三个section,以 // slots 开头的这段。这个section中的数值指明了QObject这个class包含了两个slot。第四个section,以 // properties 开头的这段。这个section中的数值指明了QObject这个class包含有一个属性定义。第五个section,以 // constructors 开头的这段,指明了QObject这个class有两个constructor。

     (2)   virtual QObject::metaObject(); 这个的方法是返回一个该类对应的staticMetaObject的const 指针。也就是引用。

如果说这个类没有定义Q_OBJECT 宏定义,那么这个类就没有staticMetaObject这个静态的QMetaObject对象,那么这个时候这实例调用返回是是其父类的QMetaObject对象,因为这个虚函数没有被重载。所以一般建议都加上这个宏定义。

      (3)qt_static_metacall()是一个静态函数,主要是用于对象的函数的调用的。Qt 5.12版本或少以上,这个静态函数也会通过指针的方式存储在QMetaObject 里面了。qt_static_metacall()提供了两种的函数调用的方式,一个是InvokeMetaMethod调用,会将对象转换为实际的对象类型,然后调用。另外一个是IndexOfMethod调用,会通过元函数的索引号调用。

 


Qt的slot和singnal

qt的slot和singnal的名称都会存在的QMetaObject中,但是这个时候,slot和signal是没有联系的。需要进行connect才可以使用。

 例如:connect(ui->pushBtn,SIGNAL(clicked()),ui->lineEdit,SLOT(clear()));

 connect 在幕后到底都做了些什么事情?为什么emit一个signal后,相应的slot都会被调用?

SIGNAL和SLOT宏定义

connect(&obj, SIGNAL(destroyed()), &app, SLOT(aboutQt()));

在这里signal和slot的名字都被包含在了两个大写的SIGNAL和SLOT中,这两个是什么呢?原来SIGNAL 和 SLOT 是Qt定义的两个宏。

# define SLOT(a)      ”1″#a  
# define SIGNAL(a)   ”2″#a  

Qt把signal和slot都转化成了字符串,并且还在这个字符串的前面加上了附加的符号,signal前面加了’2’,slot前面加了’1’。也就是说,我们前面写了下面的connect调用,在经过moc编译器转换之后,就便成了:
connect(&obj, “2destroyed()”, &app, “1aboutQt()”)); 当connect函数被调用了之后,都会去检查这两个参数是否是使用这两个宏正确的转换而来的,它检查的根据就是这两个前置数字,是否等于1或者是2,如果不是,connect函数当然就会失败啦!

 然后,会去检查发送signal的对象是否有这个signal,方法就是查找这个对象的class所对应的staticMetaObject对象中所包含的d.stringdata所指向的字符串中是否包含这个signal的名字,在这个检查过程中,就会用到d.data所指向的那一串整数,通过这些整数值来计算每一个具体字符串的起始地址。同理,还会使用同样的方法去检查slot,看响应这个signal的对象是否包含有相应的slot。这两个检查的任何一个如果失败的话,connect函数就失败了,返回false。

前面的步骤都是在做一些必要的检查工作,下一步,就是要把发送signal的对象和响应signal的对象关联起来。在QObject的私有数据类QObjectPrivate中,有下面这些数据结构来保存这些信息:

  1. class QObjectPrivate : public QObjectData
  2. {
  3.   struct Connection
  4.   {
  5.     QObject *receiver;
  6.     int method;
  7.     uint connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
  8.     QBasicAtomicPointer<int> argumentTypes;
  9.   };
  10.   typedef QList<Connection>; ConnectionList;
  11.   QObjectConnectionListVector *connectionLists;
  12.   struct Sender
  13.   {
  14.     QObject *sender;
  15.     int signal;
  16.     int ref;
  17.   };
  18.   QList<Sender> senders;
  19. };

    在发送signal的对象中,每一个signal和slot的connection,都会创建一个QObjectPrivate::Connection对象,并且把这个对象保存到connectionList这个Vector里面去。

   在响应signal的对象中,同样,也是每一个signal和slot的connection,都会一个创建一个Sender对象,并且把这个对象附加在Senders这个列表中。

 

emit幕后的故事

当emit signal的时候,与这个signal相连的slot函数就会被调用,那么这个调用时如何发生的呢?

看段代码:

  1. class ZMytestObj : public QObject
  2. {
  3.   Q_OBJECT
  4.   signals:
  5.   void sigMenuClicked();
  6.   void sigBtnClicked();
  7. };
  1. void ZMytestObj::sigMenuClicked()
  2. {
  3.   QMetaObject::activate(this,&staticMetaObject,0,0);
  4. }
  5. void ZMytestObj::sigBtnClicked()
  6. {
  7.   QMetaObject::activate(this,&staticMetaObject,1,0);
  8. }

   每一个signal都会被转换为一个与之相对应的成员函数。也就是说,当我们写下这样一行代码:emit sigBtnClicked();当程序运行到这里的时候,实际上就是调用了void ZMytestObj::sigBtnClicked() 这个函数。

  void ZMytestObj::sigMenuClicked()  void ZMytestObj::sigBtnClicked(),它们唯一的区别就是调用 QMetaObject::activate 函数时给出的参数不同,一个是0,一个是1,它们的含义是什么呢?它们表示是这个类中的第几个signal被发送出来了,回头再去看头文件就会发现它们就 是在这个类定义中,signal定义出现的顺序,这个参数可是非常重要的,它直接决定了进入这个函数体之后所发生的事情。

当执行流程进入到QMetaObject::activate函数中后,会先从connectionLists这个变量中取出与这个signal相对应的connection list,它根据的就是刚才所传入进来的signal index。这个connection list中保存了所有和这个signal相链接的slot的信息,每一对connection(即:signal和slot的连接)是这个list中的一项。

在每个一具体的链接记录中,还保存了这个链接的类型,是自动链接类型,还是队列链接类型,或者是阻塞链接类型,不同的类型处理方法还不一样的。

  1. if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
  2. || (c->connectionType == Qt::QueuedConnection)) {
  3. // 队列处理
  4. } else if (c->connectionType == Qt::BlockingQueuedConnection) {
  5. // 阻塞处理
  6. // 如果同线程,打印潜在死锁。
  7. } else {
  8. //直接调用槽函数或回调函数
  9. }

    如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。

  如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)

   队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent

   放进了事件循环, 等到下一次事件分发,相应的线程才会去调用槽函数。

    对于直接链接的类型,先找到接收这个signal的对象的指针,然后是处理这个signal的slot的index,已经是否有需要处理的参数,然后就使用这些信息去调用receiver的qt_metcall 方法。在qt_metcall方法中就简单了,根据slot的index,一个大switch语句,调用相应的slot函数就OK了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多