Qt 信号和槽底层原理源码解析笔记

1. 底层数据结构——建立连接时建立的什么。

读者可先大致浏览一下qobject_p.cpp中添加连接的实现,回头再细看:

void QObjectPrivate::addConnection(int signal, Connection *c)
{
    /*
    * 中心数据结构是二维广义表
    * 不妨看作一个二维数组:
    *  A B C D E...
    * 0
    * 1
    * 2
    * 3
    * 4
    * ...
    * 纵向序号0123..为signal序号,每一个signal对应一个connectionList(无s)单链表。
    * 所有conectionList 存放在类的 connectionLists这个向量中。
    * 横向序号为接收者reciver,相应每一列为该reciver对应接收signal的connection节点(有的话才分配,顺序不一定按0123)
    * 每列组成相应纵向单链表senders(含义为该reciver的senders)。
    * 每个对象自己保存着自己的senders单链表
    * 该二维表为全局数据结构,包含以signal索引的行链表connectionLists和每个对象自己保存的相应senders链表
    * 
    */
    Q_ASSERT(c->sender == q_ptr);
    if (!connectionLists)
        connectionLists = new QObjectConnectionListVector();
    if (signal >= connectionLists->count())
        connectionLists->resize(signal + 1);

    ConnectionList &connectionList = (*connectionLists)[signal];
    if (connectionList.last) {
        connectionList.last->nextConnectionList = c;
    } else {
        connectionList.first = c;
    }
    connectionList.last = c;

    cleanConnectionLists();
    /*
    * 插入节点的技巧:
    * prev为二级指针,指向本对象的senders列表头的地址addr(addr中保存的是表头节点的地址)
    * senders表本身为一个单链表。
    * 但链表中每一个节点的prev都指向表头地址addr,即访问某一个节点,也可以继续访问整个senders表。
    * 更新senders表时,直接将c插到原表头,原表头变为c的next。addr内容变为c的地址,但addr本身地址不变。
    */
    c->prev = &(QObjectPrivate::get(c->receiver)->senders);
    c->next = *c->prev;
    *c->prev = c;
    if (c->next)
        c->next->prev = &c->next;

    if (signal < 0) {
        connectedSignals[0] = connectedSignals[1] = ~0;
    } else if (signal < (int)sizeof(connectedSignals) * 8) {
        connectedSignals[signal >> 5] |= (1 << (signal & 0x1f));
    }
}

先看看传入参数:
传入参数signal就是所谓的信号,是一个整数。
Connection 类的定义,在qobject_p.h中。
它是在QObjectPrivate类中定义的一个结构体:

struct Connection
    {
        QObject *sender;
        QObject *receiver;
        union {
            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;
            }
        }
    };

可以看到,与链表相关的有三个指针,其中prev是个二级指针。为什么这么用,在addConnection函数我写的注释里可以看到一些解释。其他的结构体参数在这里不作深究,有些也可以见名知意。

采用二维表的目的有二:

  • 能根据发出的信号找到所有接受者
  • 能将某一个接受者接收的所有信号统一管理

采用链表的目的,自然就是方便动态管理了。

既然知道了建立的是什么数据结构,那么下一步就是理清是怎么建立的。
另外,我们都知道Qt中基本上所有的类都继承于QObject这个类,这个类包含些什么?为什么连接的数据结构是存在QObjectPrivate这个类中?QObjectPrivate和QObject是什么关系?为什么要这样定义?这是我们最后要讨论的问题。

2. 从信号和槽的定义到整数的转换

(1) 神奇的关键字

再看信号和槽的基本定义和使用方式。
一个简单例子:
定义一个 Counter类

class Counter : public QObject
{
    Q_OBJECT
    int m_value;
public:
    int value() const { return m_value; }
public slots: //使用public slots声明槽函数
    void setValue(int value);
signals: //使用signal声明信号,也是函数
    void valueChanged(int newValue);
};

信号的发射方式。在某处发射信号的写法:

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        //使用emit关键字发射信号,附带参数。
        emit valueChanged(value); 
    }
}

有了信号和槽,还需要将它们建立连接关系:

Counter a, b;
  QObject::connect(&a, SIGNAL(valueChanged(int)),
                   &b, SLOT(setValue(int)));

//可省略QObject::默认的是同一个函数
// 信号和对应的槽,参数要一致
connect(&a, &a->valueChanged,&b, b->setValue);

首先来看这几个Qt中特有的关键字的含义:slots,signals,emit
no-keywords.h中:

#define signals Q_SIGNALS
#define slots Q_SLOTS
#define emit Q_EMIT

具体地:

# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)   //主要有个public
//进一步可见:
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)

#define Q_EMIT  //(空)

简单来说,emit就只是个写程序用到的逻辑关键字,起提示作用。
而signals除了public关键字声明为公有,其他部分就只在预处理过程中起作用。
slots就是只有预处理作用。

由此可见,定义的信号和槽函数, 最终变成了整数,而定义的特殊性,仅在于多一个预处理特征(包括#define xx本身)。那么可以推测,是使用某种预处理机制,进行了转化。那么这是什么机制?如何运作的?

(2) Qt 的MOC(the Meta Object Compiler)预处理器

文档翻译时间:

Qt信号/槽和属性系统基于在运行时内省对象的能力。内省意味着能够列出对象的方法和属性,并具有关于它们的各种信息,例如它们的参数类型。
C ++本身不提供内省支持,因此Qt附带了一个提供它的工具。该工具是MOC。它是一个代码生成器(而不是像某些人所说的预处理器)。它解析头文件并生成一个额外的C ++文件,该文件与程序的其余部分一起编译。生成的C ++文件包含内省所需的所有信息。

引用说那不是预处理,但是我仍然认为,既然需要用到了宏定义,先对代码进行一遍处理(生成相关代码也是一种处理),仍然可以看作是预处理,只不过是全部预处理的一部分。

再来看每个基于QObject类都需要声明的一个宏Q_OBJECT:

#define Q_OBJECT \
public: \
    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 /* translations helper */ \
private: \
    Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);

Q_OBJECT定义了一堆函数和一个静态QMetaObject这些函数在MOC生成的文件中实现。

简单说,就是MOC根据代码的关键字,自动提取出信号和槽,并进行处理,生成了相应的cpp文件,相关要使用的函数即由Q_OBJECT定义,也生成在相应cpp文件中,随整个工程一同进行编译链接。

对于老版本的信号和槽:

Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a)     "1"#a
# define SIGNAL(a)   "2"#a
#endif

这些宏只是使用预处理器将参数转换为字符串,并在前面添加代码。在debug模式下,如果信号连接不起作用,我们还会使用文件位置注释字符串以显示警告消息。这是以兼容的方式在Qt 4.5中添加的。为了知道哪些字符串具有行信息,我们使用qFlagLocation,它将在具有两个条目的表中注册字符串地址。

也即直接将函数名翻译成字符串,在前面加1或2字符,所谓的信号和槽,也就是相应的字符串。另外只需要处理参数传递问题。

新版用法中,传递的也是函数名,但从方式上看并没有转换成字符串,而是直接传递函数指针。总之,就是两种区分名称的方法而以。函数指针方式能方便编辑器和编译器检查语法错误,使用更安全。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容