Qt的信号与槽的工作机制

因英语水平有限,但是发现该文讲的很是精彩,故转载翻译过来,如有疏漏错误的地方请多多指正

原文出处:http://woboq.com/blog/how-qt-signals-slots-work.html



1.序言

Qt众所周知的就是他的信号和槽的机制,但是它是怎么工作的呢?在这篇博文中,我们将深入探索一下信号与槽机值里面的的QObject与QMetaObject底层是如何实现的。

在这片文章中,我(作者)将展示QT5的代码,有些代码为了简洁与格式的需要,做了一定的修改。


2.信号与槽(Signals and Slots)

首先让我们来重新看一下Qt官方的信号与槽的例子

头文件里面大致时这样子的:

<span style="font-family:Comic Sans MS;">class Counter : public QObject
{
    Q_OBJECT
    int m_value;
public:
    int value() const { return m_value; }
public slots:
    void setValue(int value);
signals:
    void valueChanged(int newValue);
};

Somewhere in the .cpp file, we implement setValue()

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}
</span>
那么这个Counter类的信号与槽可以这样来使用:
<span style="font-family:Comic Sans MS;">Counter a, b;
  QObject::connect(&a, SIGNAL(valueChanged(int)),
                   &b, SLOT(setValue(int)));

  a.setValue(12);  // a.value() == 12, b.value() == 12</span>
这样连接信号与槽的写法从1992年Qt创建开始就从未被更改过。

尽管API借口从未发生改变,但是底层的实现已经历经几次更改,一些新的特性已经加入到底层的实现之中,这也不是很难懂的,我将在这篇文章中展示它是如何工作的。


3.MOC,元对象编译器(Meta Object Complier)

Qt的信号与槽和属性系统都是基于对象在运行时的内省(introspection),内省功能是对于实现信号和槽时必须的,并且允许应用程序的开发人员在运行时获得有关QObject的子类的“元信息”,包括一个含有对象的类名以及它所支持的信号与槽的列表。这一机制也广泛的支持属性和文本翻译,同时也为QtScript和QML奠定了基础。

C++自身时不提供内省的,所以Qt提供了一个工具,就是MOC。该工具将会自动的注入一些元对象的必要代码(不像是用户主动的调用与处理器一样)。

元对象编译器解析头文件(包含类,比如上面定义的Counter)并且自动生成一个额外的C++文件,该文件将会和客户编写的代码一起进行编译链接,这个新产生的C++源文件将包含内省需要的的所有信息。

Qt有时会因为这个额外的自动生成代码的机制被那些所谓的纯粹的语言学家批评,那是因为他们没有看过这篇文章《Why Doesn't Qt Use Templates for Signals and Slots?》,MOC对于Qt来说不仅没有拖了后腿,而且还使其如虎添翼。


4.神奇的宏定义(Magic Macros)


我们能列举出在Qt里面不是C++原生的关键字么?signals,slots,Q_OBJECT,emit,SIGNAL,SLOT,这些被当做是QT对于C++的一种拓展,这些个宏定义在Qt的源码文件qobjectdefs.h(/usr/include/qt4/QtCore/qobjectdefs.h)里面。
<span style="font-family:Comic Sans MS;">#define signals public
#define slots /* nothing */</span>
这是对的,信号和槽是普通的函数,编译器将像其他不同的函数一样处理他们,这些宏定义将会通过MOC展示出来。
在5.0版本之前,信号是被保护(protected:)起来的,为了适应Qt5里面的新语法,信号将变成公共的(public:)。
下面我们来展开qobjectdefs.h里面的内容看一下:

<span style="font-family:Comic Sans MS;">#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 **);

</span>

Q_OBJECT声明了一些函数以及一个QMetaObject,这些函数将会在MOC自动生成的C++代码里面进行定义。
<span style="font-family:Comic Sans MS;">#define emit /* nothing */</span>

emit是一个空的宏定义,这个宏定义不被MOC解析,换句话说,emit关键字时一个可选的选项,这样写可以使程序更好的表达它的意思。

<span style="font-family:Comic Sans MS;">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</span>

这些宏定义就是为了在预处理的过程中将参数转化为字符串(#在预处理的过程中可将参数转化为字符串),并加入一些码制在字符串的前面。

在调试模式的时候,我们也会注释掉文件定位信息这一行(

<span style="font-family:Comic Sans MS;"># define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)</span>
),这样如果信号与槽不正常工作的话,那么也会正常的发出警告信息。这个方法在Qt4.5的版本里面就已经包含了(译者注:这个怎么实现呀?,知道的告知一下),为了知道信号与槽的函数对应代码中的哪一行,我们用qFlagLocation函数来将SLOT(a)字符串和SIGNAL(a)字符串(字符串里面包含了信号与槽的函数名字以及各自的行号)的地址注册一下,方便后面查找信号和槽的需要。下面给出qFlagLocation函数:

<span style="font-family:Comic Sans MS;">const char *qFlagLocation(const char *method)     
{     
    static int idx = 0;//static类型的,每次进入该函数都会使用上一次退出该函数时的值
    flagged_locations[idx] = method;     
    idx = (idx+1) % flagged_locations_count;     
    return method;     
}</span>

5.MOC自动生成的代码(MOC Generated Code)


我们接下来看一下MOC在Qt5里面产生的代码

QMetaObject

<span style="font-family:Comic Sans MS;">const QMetaObject Counter::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_Counter.data,
      qt_meta_data_Counter,  qt_static_metacall, 0, 0}
};


const QMetaObject *Counter::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}</span>

我们看到了这里实现了Counter::metaObject()和Counter::staticMetaObject,他们在一开始Q_OBJECT宏里面就已经定义了。QObject::d_ptr->metaObject仅仅只有在QML里面使用,所以一般来说,虚函数metaObject()只是返回staticMetaObject这个类,而且staticMetaObject实在只读数据区里面构建的。
QMetaObject这个类定义在qobjectdefs.h这个文件中:
<span style="font-family:Comic Sans MS;">struct QMetaObject
{
    /* ... Skiped all the public functions ... */

    enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };

    struct { // private data
        const QMetaObject *superdata;
        const QByteArrayData *stringdata;
        const uint *data;
        typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
        StaticMetacallFunction static_metacall;
        const QMetaObject **relatedMetaObjects;
        void *extradata; //reserved for future use
    } d;
};</span>

d表明所有的成员应该都是私有的(private),为了满足POD(译者注:Plain old data structure, 缩写为POD, 是C++语言的标准中定义的一类数据结构[1],POD适用于需要明确的数据底层操作的系统中。POD通常被用在系统的边界处,即指不同系统之间只能以底层数据的形式进行交互,系统的高层逻辑不能互相兼容。比如当对象的字段值是从外部数据中构建时,系统还没有办法对对象进行语义检查和解释,这时就适用POD来存储数据。)和允许静态初始化的需求,这里将他们声明成public的。
QMetaObject被父对象的元数据(superdata)初始化(QObject::staticMetaObject也是这样),stringdata和data被一些文章后面提到的一些数据初始化,static_metacall是一个函数指针,且被初始化为Counter::qt_static_metacall.

(内省表)Introspection Tables

首先我们分析一下在元对象里面的整形数据:
<span style="font-family:Comic Sans MS;">static const uint qt_meta_data_Counter[] = {

 // content:
       7,       // 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,    1,   24,    2, 0x05,

 // slots: name, argc, parameters, tag, flags
       4,    1,   27,    2, 0x0a,

 // signals: parameters
    QMetaType::Void, QMetaType::Int,    3,

 // slots: parameters
    QMetaType::Void, QMetaType::Int,    5,

       0        // eod
};</span>


一开始的13个int类型的数据组成了头部,当有两列的情况下,第一行表示个数,第二行表示在项描述数组里面的开始索引。
这里,我们的Counter类有两个方法,并且关于方法的描述信息开始于第14个int数据。
每个方法的描述都是由五个int类型来描述的。第一个数方法名字,那是在一个字符串表里面的索引值(我们稍后再看细节),第二个整形描述的是参数的个数,接下来的那个整形是参数描述的索引,我们现在将会忽略tag和flags的索引,对于每一个函数,moc也存储他们的返回类型,参数类型以及他们的名字。

字符串表(String Table)

struct qt_meta_stringdata_Counter_t {
    QByteArrayData data[6];
    char stringdata[47];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    offsetof(qt_meta_stringdata_Counter_t, stringdata) + ofs \
        - idx * sizeof(QByteArrayData) \
    )
static const qt_meta_stringdata_Counter_t qt_meta_stringdata_Counter = {
    {
QT_MOC_LITERAL(0, 0, 7),
QT_MOC_LITERAL(1, 8, 12),
QT_MOC_LITERAL(2, 21, 0),
QT_MOC_LITERAL(3, 22, 8),
QT_MOC_LITERAL(4, 31, 8),
QT_MOC_LITERAL(5, 40, 5)
    },
    "Counter\0valueChanged\0\0newValue\0setValue\0"
    "value\0"
};
#undef QT_MOC_LITERAL

这里的工作基本上就是将字符串   "Counter\0valueChanged\0\0newValue\0setValue\0""value\0"里面的以'\0'为分割符,一个一个的放在QByteArray类型的数组里面。

信号(Signals)

MOC也实现了信号,他们只是一些简单的函数,只要负责创建一个指向参数的数组并将他们传递给QMetaObject::activate()函数,第一个是函数的返回值,在我们的例子里面,因为函数的返回值是void,所以把他设为0,第三个传给activate函数的参数是信号索引值index(这里是0)
// SIGNAL 0
void Counter::valueChanged(int _t1)
{
    void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

调用一个槽(Calling a Slot)

在qt_static_metacall里面通过索引来调用一个槽也是可能的:

void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Counter *_t = static_cast<Counter *>(_o);
        switch (_id) {
        case 0: _t->valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: _t->setValue((*reinterpret_cast< int(*)>(_a[1]))); break;
        default: ;
 }
这里的数组指针指向的参数和在signal里面的指针具有相同的格式,这里没有用到_a[0],因为返回值是void。

索引注意事项( Note About Indexes)

在每一个元对象里面,槽,信号和其他函数方法都由一个索引值来索引,并且从0开始。他们按照信号第一,槽第二,接下来的是其他函数的顺序来在数组里面排序。这些索引通过内部的相对索引来调用,他们并不包含父对象的索引(一个派生类的对象里面包含了基类的对象)。
但是一般来说,我们对于对于一个特殊的类来说,我们想知道的是全局的绝对索引,而不是相对索引。从继承链里面包含了一些方法,方便我们从相对索引得到绝对索引。这个绝对索引就是在Qt的API中使用的, 像QMetaObject::indexOf{Signal,Slot,Method}这样的函数返回的就是绝对索引。
连接机制通过使用一个由signal来索引的容器vector,但是如果将所有的槽都放在容器里面将会浪费内存,通常槽的数量都要比信号的数量多,所以从4.6开始,就多了一个内部的信号索引来专门索引信号。
在开发Qt的时候,你可能仅仅只需要知道方法的绝对索引,但是你在浏览Qt源码的时候,你必须知道他们之间的不同。

连接是如何工作的(How Connecting Works)

在Qt处理信号与槽的连接的时候,首先要做的是找到信号与槽的索引,Qt将会元对象里面的string tables来找到对应的索引。
接着一个QObjectPrivate::Connection对象穿件,并将它加入到链表里面。
对于每一个连接了来说,都需要些什么信息呢?我们需要通过信号索引来迅速的和槽建立通信,因为每一个信号可以和几个槽相连接,我们需要一个与信号对应的槽的链表。每一个连接必须包含接受对象并且和槽的索引值,我们想在接收对象被销毁的时候也自动的将之间建立的连接销毁断开,所以每一个信号的接收对象需要知道他的信号连接对象一边在自身销毁的时候断开连接。
这里是QObjectPrivate::Connection在qobject_p.h文件里面的定义:

struct QObjectPrivate::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(0), 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 { return method_offset + method_relative; }
    void ref() { ref_.ref(); }
    void deref() {
        if (!ref_.deref()) {
            Q_ASSERT(!receiver);
            delete this;
        }
    }
}
每一个对象都有一个连接容器:这是一个对于对象里面的每一个信号建立起来的QObjectPrivate::Connection类型的链表。
每一个对象也有一个为了自动删除连接的双指针的反向链表。
原理如下图:

这里使用链表是因为需要快速的删除和移动连接对象,这个链表是通过在QObjectPrivate::Connection里面的next/previous指针建立起来的。
注意这里prev指针是一个指向指针的指针,这是因为不是真正的指向上一个节点,而是指向上一个节点的next指针,这个指针仅仅在连接摧毁的时候才被使用,这样的话就不用使用表头来做特别的操作了。

信号的发射(Signal Emission)

当我们触发一个信号,我们通过用MOC调用QMetaObject::activate()函数来自动产生代码。
这里是一个有注释的qobject.cpp文件
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
                           void **argv)
{
    activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv);
    /* We just forward to the next function here. We pass the signal offset of
     * the meta object rather than the QMetaObject itself
     * It is split into two functions because QML internals will call the later. */
}

void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;

    /* The first thing we do is quickly check a bit-mask of 64 bits. If it is 0,
     * we are sure there is nothing connected to this signal, and we can return
     * quickly, which means emitting a signal connected to no slot is extremely
     * fast. */
    if (!sender->d_func()->isSignalConnected(signal_index))
        return; // nothing connected to these signals, and no spy

    /* ... Skipped some debugging and QML hooks, and some sanity check ... */

    /* We lock a mutex because all operations in the connectionLists are thread safe */
    QMutexLocker locker(signalSlotLock(sender));

    /* Get the ConnectionList for this signal.  I simplified a bit here. The real code
     * also refcount the list and do sanity checks */
    QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
    const QObjectPrivate::ConnectionList *list =
        &connectionLists->at(signal_index);

    QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // We need to check against last here to ensure that signals added
    // during the signal emission are not emitted in this emission.
    QObjectPrivate::Connection *last = list->last;

    /* Now iterates, for each slot */
    do {
        if (!c->receiver)
            continue;

        QObject * const receiver = c->receiver;
        const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;

        // determine if this connection should be sent immediately or
        // put into the event queue
        if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
            || (c->connectionType == Qt::QueuedConnection)) {
            /* Will basically copy the argument and post an event */
            queued_activate(sender, signal_index, c, argv);
            continue;
        } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            /* ... Skipped ... */
            continue;
        }

        /* Helper struct that sets the sender() (and reset it backs when it
         * goes out of scope */
        QConnectionSenderSwitcher sw;
        if (receiverInSameThread)
            sw.switchSender(receiver, sender, signal_index);

        const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
        const int method_relative = c->method_relative;
        if (c->isSlotObject) {
            /* ... Skipped....  Qt5-style connection to function pointer */
        } else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
            /* If we have a callFunction (a pointer to the qt_static_metacall
             * generated by moc) we will call it. We also need to check the
             * saved metodOffset is still valid (we could be called from the
             * destructor) */
            locker.unlock(); // We must not keep the lock while calling use code
            callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
            locker.relock();
        } else {
            /* Fallback for dynamic objects */
            const int method = method_relative + c->method_offset;
            locker.unlock();
            metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
            locker.relock();
        }

        // Check if the object was not deleted by the slot
        if (connectionLists->orphaned) break;
    } while (c != last && (c = c->nextConnectionList) != 0);
}

结论(Conclusion)

我们已经学习了连接怎么建立的和信号与槽是如何触发的,我们还没有学习Qt5中的新语法特性实现,但是我们将会在另外一篇文章来介绍
这篇新的博文在看这里:http://woboq.com/blog/how-qt-signals-slots-work-part2-qt5.html

你可能感兴趣的:(C++,qt,元对象系统)