zoukankan      html  css  js  c++  java
  • Qt信号槽机制源码解析

    Qt信号槽机制源码解析

    来源 https://blog.51cto.com/9291927/2070398

    一、信号槽机制的原理

    1、信号槽简介

    信号槽是观察者模式的一种实现,特性如下:
    A、一个信号就是一个能够被观察的事件,或者至少是事件已经发生的一种通知;
    B、一个槽就是一个观察者,通常就是在被观察的对象发生改变的时候——也可以说是信号发出的时候——被调用的函数;
    C、信号与槽的连接,形成一种观察者-被观察者的关系;
    D、当事件或者状态发生改变的时候,信号就会被发出;同时,信号发出者有义务调用所有注册的对这个事件(信号)感兴趣的函数(槽)。
    信号和槽是多对多的关系。一个信号可以连接多个槽,而一个槽也可以监听多个信号。
    信号槽与语言无关,有多种方法可以实现信号槽,不同的实现机制会导致信号槽的差别很大。信号槽术语最初来自 Trolltech 公司的 Qt 库,由于其设计理念的先进性,立刻引起计算机科学界的注意,提出了多种不同的实现。目前,信号槽依然是 Qt 库的核心之一,其他许多库也提供了类似的实现,甚至出现了一些专门提供这一机制的工具库。
      信号槽是Qt对象以及其派生类对象之间的一种高效通信接口,是Qt的核心特性,也是Qt区别与其他工具包的重要地方。信号槽完全独立于标准的C/C++语言,因此要正确的处理好信号和槽,必须借助于一个成为MOC(Meta Object Compiler)的Qt工具,MOC工具是一个C++预处理程序,能为高层次的事件处理自动生成所需要的附加代码。

    2、不同平台的实现

    MFC中的消息机制没有采用C++中的虚函数机制,原因是消息太多,虚函数开销太大。在Qt中也没有采用C++中的虚函数机制,而是采用了信号槽机制,原因与此相同。更深层次的原因上,多态的底层实现机制只有两种,一种是按照名称查表,一种是按照位置查表。两种方式各有利弊,而C++的虚函数机制无条件的采用了后者,导致的问题就是在子类很少重载基类实现的时候开销太大,再加上界面编程中子类众多的情况,基本上C++的虚函数机制效率太低,于是各家库的编写者就只好自谋生路,当然,这其实是C++语言本身的缺陷。

    二、Qt信号槽实例解析

    1、信号槽使用示例

    使用简单的实例:

    #ifndef OBJECT_H
    #define OBJECT_H
    
    #include <QObject>
    #include <QString>
    #include <QDebug>
    
    class Object : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(int age READ age  WRITE setAge NOTIFY ageChanged)
        Q_PROPERTY(int score READ score  WRITE setScore NOTIFY scoreChanged)
        Q_PROPERTY(Level level READ level WRITE setLevel)
        Q_CLASSINFO("Author", "Scorpio")
        Q_CLASSINFO("Version", "1.0")
    public:
        enum Level
        {
            Basic = 1,
            Middle,
            Advanced,
            Master
        };
        Q_ENUMS(Level)
    protected:
        QString m_name;
        Level m_level;
        int m_age;
        int m_score;
        void setLevel(const int& score)
        {
            if(score <= 60)
            {
                m_level = Basic;
            }
            else if(score < 100)
            {
                m_level = Middle;
            }
            else if(score < 150)
            {
                m_level = Advanced;
            }
            else
            {
                m_level = Master;
            }
        }
    public:
        explicit Object(QString name, QObject *parent = 0):QObject(parent)
        {
            m_name = name;
            setObjectName(m_name);
            connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int)));
            connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int)));
        }
    
        int age()const
        {
            return m_age;
        }
    
        void setAge(const int& age)
        {
            m_age = age;
            emit ageChanged(m_age);
        }
    
        int score()const
        {
            return m_score;
        }
    
        void setScore(const int& score)
        {
            m_score = score;
            setLevel(m_score);
            emit scoreChanged(m_score);
        }
    
        Level level()const
        {
            return m_level;
        }
    
        void setLevel(const Level& level)
        {
            m_level = level;
        }
    signals:
        void ageChanged(int age);
        void scoreChanged(int score);
    public slots:
         void onAgeChanged(int age)
         {
             qDebug() << "age changed:" << age;
         }
         void onScoreChanged(int score)
         {
             qDebug() << "score changed:" << score;
         }
    };
    
    #endif // OBJECT_H

    Main函数:

    #include <QCoreApplication>
    #include "Object.h"
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
        Object ob("object");
    
        //设置属性age
        ob.setProperty("age", QVariant(30));
        qDebug() << "age: " << ob.age();
        qDebug() << "property age: " << ob.property("age").toInt();
    
        //设置属性score
        ob.setProperty("score", QVariant(90));
        qDebug() << "score: " << ob.score();
        qDebug() << "property score: " << ob.property("score").toInt();
    
        qDebug() << "Level: " << ob.level();
        ob.setProperty("level", 4);
        qDebug() << "level: " << ob.level();
        qDebug() << "Property level: " << ob.property("level").toInt();
    
        //内省intropection,运行时查询对象信息
        qDebug() << "object name: " << ob.objectName();
        qDebug() << "class name: " << ob.metaObject()->className();
        qDebug() << "isWidgetType: " << ob.isWidgetType();
        qDebug() << "inherit: " << ob.inherits("QObject");
    
        return a.exec();
    }

    2、SIGNAL与SLOT宏

    SIGNAL与SLOT宏定义在/src/corelib/kernel/Qobjectdefs.h文件中。

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

    SIGNAL与SLOT宏会利用预编译器将一些参数转化成字符串,并且在前面添加上编码。
    在调试模式中,如果signal的连接出现问题,提示警告信息的时候还会注明对应的文件位置。qFlagLocation 用于定位代码对应的行信息,会将对应代码的地址信息注册到一个有两个入口的表里。
    Object.h文件中有关SIGNAL与SLOT宏部分代码如下:

     connect(this, SIGNAL(ageChanged(int)), this, SLOT(onAgeChanged(int)));
         connect(this, SIGNAL(scoreChanged(int)), this, SLOT(onScoreChanged(int)));

    通过对Object.h文件进行预编译,得到Object.i文件。
    使用G++进行预编译:
    g++ -E Object.h -o Object.i -I/usr/local/Trolltech/Qt-4.8.6/include/QtCore -I/usr/local/Trolltech/Qt-4.8.6/include -I.
    Object.i文件中结果如下:

    connect(this, qFlagLocation("2""ageChanged(int)" "" "Object.h" ":" "54"), this, qFlagLocation("1""onAgeChanged(int)" "" "Object.h" ":" "54"));
    connect(this, qFlagLocation("2""scoreChanged(int)" "" "Object.h" ":" "55"), this, qFlagLocation("1""onScoreChanged(int)" "" "Object.h" ":" "55"));

    3、类的元对象

    程序编译时make调用MOC对工程源码进行解析,生成相应类的moc_xxx.cpp文件,

    const QMetaObject Object::staticMetaObject = {
        { &QObject::staticMetaObject, qt_meta_stringdata_Object,
          qt_meta_data_Object, &staticMetaObjectExtraData }
    };

    静态成员staticMetaObject被填充的值如下:
          const QMetaObject superdata;//元数据代表的类的基类的元数据,被填充为基类的元数据指针&QWidget::staticMetaObject
        const char 
    stringdata;//元数据的签名标记,被填充为qt_meta_stringdata_Widget.data
        const uint *data;//元数据的索引数组的指针,被填充为qt_meta_data_Widget
            const QMetaObject **extradata;//扩展元数据表的指针,内部被填充为函数指针qt_static_metacall。
    staticMetaObjectExtraData初始化如下:

    const QMetaObjectExtraData Object::staticMetaObjectExtraData = {
        0,  qt_static_metacall 
    };

    QMetaObjectExtraData类型的内部成员static_metacall是一个指向Object::qt_static_metacall 的函数指针。
    Object的内存布局如下:

    Object内存布局已经包含了静态成员staticMetaObject和
    staticMetaObjectExtraData成员。

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

    QObject::d_ptr->metaObject仅供动态元对象(QML对象)使用,所以一般而言,虚函数 metaObject() 仅返回类的 staticMetaObject。

    4、元数据表

    Qt程序编译时make会调用MOC工具对源文件进行分析,如果某个类包含了Q_OBJECT宏,MOC会生成对应的moc_xxx.cpp文件。
    moc_Object.cpp文件内容中:
    Object的元数据如下:

    static const uint qt_meta_data_Object[] = {
    
     // content:内容信息
           6,       // revision        MOC生成代码的版本号
           0,       // classname      类名,在qt_meta_stringdata_Object数组中索引为0
           2,   14, // classinfo       类信息,有2个cassinfo定义,
           4,   18, // methods         类有4个自定义方法,即信号与槽个数,
           3,   38, // properties      属性的位置信息,有3个自定义属性,
           1,   50, // enums/sets      枚举的位置信息,有一个自定义枚举,在qt_meta_stringdata_Object数组中索引为50
           0,    0, // constructors    构造函数的位置信息
           0,       // flags
           2,       // signalCount
    
     // classinfo: key, value      //类信息的存储在qt_meta_stringdata_Object数组中,
          15,    7,            //第一个类信息,key的数组索引为15,即Author,value的数组索引为7,即Scorpio
          26,   22,            //第二个类信息,key的数组索引为26,即Version,value的数组索引为22,即1.0
    
     // signals: signature, parameters, type, tag, flags
          39,   35,   34,   34, 0x05,   //第一个自定义信号的签名存储在qt_meta_stringdata_Object数组中,
                        //索引是39,即ageChanged(int)
          61,   55,   34,   34, 0x05,   //第二个自定义信号的签名存储在qt_meta_stringdata_Object数组中,
                        //索引是61,即scoreChanged(int)
    
     // slots: signature, parameters, type, tag, flags
          79,   35,   34,   34, 0x0a,   //第一个自定义槽函数的签名存储在qt_meta_stringdata_Object数组中,
                        //索引是79,即onAgeChanged(int)
          97,   55,   34,   34, 0x0a,   //第二个自定义槽函数的签名存储在qt_meta_stringdata_Object数组中,
                        //索引是79,即onScoreChanged(int)
     // properties: name, type, flags
          35,  117, 0x02495103,         // 第一个自定义属性的签名存储在qt_meta_stringdata_Object中,索引是35,即age
          55,  117, 0x02495103,     // 第二个自定义属性的签名存储在qt_meta_stringdata_Object中,索引是55,即score
         127,  121, 0x0009510b,     // 第三个自定义属性的签名存储在qt_meta_stringdata_Object中,索引是127,即level
    
     // properties: notify_signal_id    //属性关联的信号编号
           0,   
           1,
           0,
    
     // enums: name, flags, count, data
         121, 0x0,    4,   54,         //枚举的定义,存储在qt_meta_stringdata_Object中,索引是121,即Level,内含4个枚举常量
    
     // enum data: key, value          //枚举数据的键值对
         133, uint(Object::Basic),     //数组索引是133,即Basic
         139, uint(Object::Middle),    //数组索引是139,即Middle
         146, uint(Object::Advanced),  //数组索引是146,即Advanced
         155, uint(Object::Master),    //数组索引是155,即Master
    
           0        // eod   元数据结束标记
    };

    内省表是一个 uint 数组,分为五个部分:第一部分content,即内容,分为9行。第一行revision,指MOC生成代码的版本号(Qt4 是6,Qt5则是7)。第二个classname,即类名,该值是一个索引,指向字符串表的某一个位置(本例中就是第0位)。

    static const char qt_meta_stringdata_Object[] = {
        "ObjectScorpioAuthor""1.0Version"
        "ageageChanged(int)scorescoreChanged(int)"
        "onAgeChanged(int)onScoreChanged(int)"
        "intLevellevelBasicMiddleAdvanced"
        "Master"
    };

    5、信号的实现

    MOC在生成的moc_xxx.cpp文件中实现了信号,创建了一个指向参数的指针的数组,并将指针数组传给QMetaObject::activate函数。数组的第一个元素是返回值。本例中值是0,因为返回值是void。传给activate函数的第三个参数是信号的索引(本例中是0)。

    // SIGNAL 0,ageChanged信号的实现
    void Object::ageChanged(int _t1)
    {
        void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
        QMetaObject::activate(this, &staticMetaObject, 0, _a);
    }
    
    // SIGNAL 1  scoreChanged信号的实现
    void Object::scoreChanged(int _t1)
    {
        void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
        QMetaObject::activate(this, &staticMetaObject, 1, _a);
    }

    6、槽函数的调用

    利用槽函数在qt_static_metacall 函数的索引位置来调用槽函数:

    void Object::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
    {
        if (_c == QMetaObject::InvokeMetaMethod) {
            Q_ASSERT(staticMetaObject.cast(_o));
            Object *_t = static_cast<Object *>(_o);
            switch (_id) {
            case 0: _t->ageChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            case 1: _t->scoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            case 2: _t->onAgeChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            case 3: _t->onScoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            default: ;
            }
        }
    }

    7、元对象中的索引

    在每一个QMetaObject对象中,槽、信号以及其它的对象可调用函数都会分配一个从0开始的索引。索引是有顺序的,信号在第一位,槽在第二位,最后是其它函数。这个索引在内部被称为相对索引,不包含父对象的索引位。
    为了实现包含在继承链中其它函数的索引,在相对索引的基础上添加一个偏移量,得到绝对索引。绝对索引是在公开API中使用的索引,由QMetaObject::indexOf(Signal, Slot, Method) 类似的函数返回。
    连接机制使用以信号为索引的向量。但是在向量中,所有的槽也会占有一定空间,通常在一个对象中,槽的数量要比信号多。所以从 Qt 4.6开始,使用的是一种仅包含信号索引的新的内部实现。

    8、信号与槽的连接

    开始连接时,Qt所要做的第一件事是找出所需要的信号和槽的索引。Qt会去查找元对象的字符串表来找出相应的索引。
    然后,创建一个 QObjectPrivate::Connection 对象,将其添加到内部的链表中。
    由于允许多个槽连接到同一个信号,需要为每一个信号添加一个已连接的槽的列表。每一个连接都必须包含接收对象和槽的索引。在接收对象销毁的时候,相应的连接也能够被自动销毁。所以每一个接收对象都需要知道谁连接到它自己,以便能够清理连接。
    QObject对象的私有数据QObjectPrivate如下:

    class Q_CORE_EXPORT QObjectPrivate : public QObjectData
    {
        Q_DECLARE_PUBLIC(QObject)
    public:
        struct ExtraData
        {
            ExtraData() {}
            QList<QByteArray> propertyNames;
            QList<QVariant> propertyValues;
        };
    
        typedef void (*StaticMetaCallFunction)(QObject *, QMetaObject::Call, int, void **);
        struct Connection
        {
            QObject *sender;
            QObject *receiver;
            StaticMetaCallFunction callFunction;
            // The next pointer for the singly-linked ConnectionList
            Connection *nextConnectionList;
            //senders linked list
            Connection *next;
            Connection **prev;
            QBasicAtomicPointer<int> argumentTypes;
            ushort method_offset;
            ushort method_relative;
            ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
            ~Connection();
            int method() const { return method_offset + method_relative; }
        };
        // ConnectionList is a singly-linked list
        struct ConnectionList {
            ConnectionList() : first(0), last(0) {}
            Connection *first;
            Connection *last;
        };
    
        struct Sender
        {
            QObject *sender;
            int signal;
            int ref;
        };
    
        QObjectPrivate(int version = QObjectPrivateVersion);
        virtual ~QObjectPrivate();
        void deleteChildren();
    
        void setParent_helper(QObject *);
        void moveToThread_helper();
        void setThreadData_helper(QThreadData *currentData, QThreadData *targetData);
        void _q_reregisterTimers(void *pointer);
    
        bool isSender(const QObject *receiver, const char *signal) const;
        QObjectList receiverList(const char *signal) const;
        QObjectList senderList() const;
    
        void addConnection(int signal, Connection *c);
        void cleanConnectionLists();
    
        static inline Sender *setCurrentSender(QObject *receiver,
                                        Sender *sender);
        static inline void resetCurrentSender(QObject *receiver,
                                       Sender *currentSender,
                                       Sender *previousSender);
        static void clearGuards(QObject *);
    
        static QObjectPrivate *get(QObject *o) {
            return o->d_func();
        }
    
        int signalIndex(const char *signalName) const;
        inline bool isSignalConnected(uint signalIdx) const;
    
        // To allow arbitrary objects to call connectNotify()/disconnectNotify() without making
        // the API public in QObject. This is used by QDeclarativeNotifierEndpoint.
        inline void connectNotify(const char *signal);
        inline void disconnectNotify(const char *signal);
    
        static inline void signalSignature(const QMetaMethod &signal,
                                           QVarLengthArray<char> *result);
    
    public:
        QString objectName;
        ExtraData *extraData;    // extra data set by the user
        QThreadData *threadData; // id of the thread that owns the object
    
        QObjectConnectionListVector *connectionLists;//连接链表向量容器
    
        Connection *senders;     // linked list of connections connected to this object
        Sender *currentSender;   // object currently activating the object
        mutable quint32 connectedSignals[2];
    
        // preserve binary compatibility with code compiled without Qt 3 support
        // keeping the binary layout stable helps the Qt Creator debugger
        void *unused;
    
        QList<QPointer<QObject> > eventFilters;
        union {
            QObject *currentChildBeingDeleted;
            QAbstractDeclarativeData *declarativeData; //extra data used by the declarative module
        };
    
        // these objects are all used to indicate that a QObject was deleted
        // plus QPointer, which keeps a separate list
        QAtomicPointer<QtSharedPointer::ExternalRefCountData> sharedRefcount;
    };

    每一个QObject对象都有一个连接链表容器QObjectConnectionListVector *connectionLists:将每一个信号与一个 QObjectPrivate::Connection 的链表关联起来。
    QObject::connect函数的实现如下:

    bool QObject::connect(const QObject *sender, const char *signal,
                          const QObject *receiver, const char *method,
                          Qt::ConnectionType type)
    {
        {
            const void *cbdata[] = { sender, signal, receiver, method, &type };
            if (QInternal::activateCallbacks(QInternal::ConnectCallback, (void **) cbdata))
                return true;
        }
    
        if (type == Qt::AutoCompatConnection) {
            type = Qt::AutoConnection;
        }
    
        if (sender == 0 || receiver == 0 || signal == 0 || method == 0) {
            qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
                     sender ? sender->metaObject()->className() : "(null)",
                     (signal && *signal) ? signal+1 : "(null)",
                     receiver ? receiver->metaObject()->className() : "(null)",
                     (method && *method) ? method+1 : "(null)");
            return false;
        }
        QByteArray tmp_signal_name;
    
        if (!check_signal_macro(sender, signal, "connect", "bind"))
            return false;
        const QMetaObject *smeta = sender->metaObject();
        const char *signal_arg = signal;
        ++signal; //skip code
        //在发送者对象的元对象中将信号的相对索引找到
        int signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal, false);
        if (signal_index < 0)
        {
            // check for normalized signatures
            tmp_signal_name = QMetaObject::normalizedSignature(signal - 1);
            signal = tmp_signal_name.constData() + 1;
    
            smeta = sender->metaObject();
            signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal, false);
        }
        if (signal_index < 0)
        {
            // re-use tmp_signal_name and signal from above
    
            smeta = sender->metaObject();
            signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal, true);
        }
        if (signal_index < 0) {
            err_method_notfound(sender, signal_arg, "connect");
            err_info_about_objects("connect", sender, receiver);
            return false;
        }
        signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index);
        int signalOffset, methodOffset;
        computeOffsets(smeta, &signalOffset, &methodOffset);
        int signal_absolute_index = signal_index + methodOffset;
        signal_index += signalOffset;
    
        QByteArray tmp_method_name;
        int membcode = extract_code(method);
    
        if (!check_method_code(membcode, receiver, method, "connect"))
            return false;
        const char *method_arg = method;
        ++method; // skip code
    
        const QMetaObject *rmeta = receiver->metaObject();
        //在接受者对象的元对象中将槽函数的相对索引找到
        int method_index_relative = -1;
        switch (membcode) {
        case QSLOT_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(&rmeta, method, false);
            break;
        case QSIGNAL_CODE:
            method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(&rmeta, method, false);
            break;
        }
    
        if (method_index_relative < 0) {
            // check for normalized methods
            tmp_method_name = QMetaObject::normalizedSignature(method);
            method = tmp_method_name.constData();
    
            // rmeta may have been modified above
            rmeta = receiver->metaObject();
            switch (membcode) {
            case QSLOT_CODE:
                method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(&rmeta, method, false);
                if (method_index_relative < 0)
                    method_index_relative = QMetaObjectPrivate::indexOfSlotRelative(&rmeta, method, true);
                break;
            case QSIGNAL_CODE:
                method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(&rmeta, method, false);
                if (method_index_relative < 0)
                    method_index_relative = QMetaObjectPrivate::indexOfSignalRelative(&rmeta, method, true);
                break;
            }
        }
    
        if (method_index_relative < 0) {
            err_method_notfound(receiver, method_arg, "connect");
            err_info_about_objects("connect", sender, receiver);
            return false;
        }
        //检查连接参数是否匹配
        if (!QMetaObject::checkConnectArgs(signal, method))
        {
            qWarning("QObject::connect: Incompatible sender/receiver arguments"
                     "
            %s::%s --> %s::%s",
                     sender->metaObject()->className(), signal,
                     receiver->metaObject()->className(), method);
            return false;
        }
    
        int *types = 0;
        if ((type == Qt::QueuedConnection)
                && !(types = queuedConnectionTypes(smeta->method(signal_absolute_index).parameterTypes())))
            return false;
        //调用QMetaObjectPrivate::connect将信号与槽进行连接
        if (!QMetaObjectPrivate::connect(sender, signal_index, receiver, method_index_relative, rmeta ,type, types))
            return false;
        const_cast<QObject*>(sender)->connectNotify(signal - 1);
        return true;
    }

    QObject::connect函数的主要功能是在接受者对象的元对象中将槽函数的相对索引找到,在接受者对象的元对象中将槽函数的相对索引找到,最后调用QMetaObjectPrivate::connect将信号与槽进行连接。QObject及其派生类对象的元对象在创建时就有一个QObjectConnectionListVector连接链表容器,QObject::connect的作用就是将新的连接加入到信号发送者附属的元对象的连接链表容器的相应信号的连接链表中(一个信号可能连接多个槽函数)。

    每个QObject及其派生类对象都有一个QObjectConnectionListVector *connectionLists连接链表容器,将信号的索引作为容器的索引,将每一个信号与一个 QObjectPrivate::ConnectionList链表关联起来。同时,QObjectPrivate::ConnectionList链表中连接的某个槽函数可能是接收者对象的槽函数链表中的一个。每个接收者对象的链表如下:

    senderList 的 prev 指针是一个指针的指针。这是因为并不是真的指向上一个节点,而是指向上一个节点中的 next 指针。这个指针仅在连接销毁时使用,并且不能向后遍历。它允许不为第一个元素添加特殊处理。
    容器中存储的ConnectionList如下:

    struct ConnectionList {
            ConnectionList() : first(0), last(0) {}
            Connection *first;//第一个结点
            Connection *last;//最后一个结点
        };

    每个ConnectionList类型元素是一个双向链表,保存了信号的所有连接。连接的类型Connection结构如下:

    struct Connection
    {
        QObject *sender;//发送者
        QObject *receiver;//接受者
        StaticMetaCallFunction callFunction;//调用的槽函数
        // The next pointer for the singly-linked ConnectionList
        Connection *nextConnectionList;
        //senders linked list
        Connection *next;
        Connection **prev;
        QBasicAtomicPointer<int> argumentTypes;
        ushort method_offset;
        ushort method_relative;
        ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
        ~Connection();
        int method() const { return method_offset + method_relative; }
    };

    QMetaObjectPrivate::connect函数源码如下:
    //将一个新的连接加入到信号发送者的连接链表容器中相应信号的连接链表中,其中连接加入的连接链表的索引为信号的索引

    bool QMetaObjectPrivate::connect(const QObject *sender, int signal_index,
                                     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;
        //在元对象的元数据字符串中找到回调的函数指针qt_static_metacall
        QObjectPrivate::StaticMetaCallFunction callFunction =
            (rmeta && QMetaObjectPrivate::get(rmeta)->revision >= 6 && rmeta->d.extradata)
            ? reinterpret_cast<const QMetaObjectExtraData *>(rmeta->d.extradata)->static_metacall : 0;
    
        QOrderedMutexLocker locker(signalSlotLock(sender),
                                   signalSlotLock(receiver));
        //如果连接类型为Qt::UniqueConnection
        if (type & Qt::UniqueConnection)
        {
            QObjectConnectionListVector *connectionLists = QObjectPrivate::get(s)->connectionLists;
            if (connectionLists && connectionLists->count() > signal_index)
            {
                //根据信号索引获取信号的连接
                const QObjectPrivate::Connection *c2 =
                    (*connectionLists)[signal_index].first;
    
                int method_index_absolute = method_index + method_offset;
    
                while (c2)
                {   //如果信号的接收者相同并且槽函数相同,即相同的连接已经存在
                    if (c2->receiver == receiver && c2->method() == method_index_absolute)
                        return false;//直接返回,
                    c2 = c2->nextConnectionList;//下一个信号连接
                }
            }
            type &= Qt::UniqueConnection - 1;
        }
        //创建一个新的连接
        QObjectPrivate::Connection *c = new QObjectPrivate::Connection;
        //设置连接的属性
        c->sender = s;
        c->receiver = r;
        c->method_relative = method_index;
        c->method_offset = method_offset;
        c->connectionType = type;
        c->argumentTypes = types;
        c->nextConnectionList = 0;
        c->callFunction = callFunction;//设置回调的函数指针为qt_static_metacall
    
        QT_TRY
        {   //将连接添加到发送者的连接链表容器中相应的信号对应的连接链表中
            QObjectPrivate::get(s)->addConnection(signal_index, c);
        } QT_CATCH(...) {
            delete c;
            QT_RETHROW;
        }
    
        c->prev = &(QObjectPrivate::get(r)->senders);
        c->next = *c->prev;
        *c->prev = c;
        if (c->next)
            c->next->prev = &c->next;
        QObjectPrivate *const sender_d = QObjectPrivate::get(s);
        if (signal_index < 0)
        {
            sender_d->connectedSignals[0] = sender_d->connectedSignals[1] = ~0;
        }
        else if (signal_index < (int)sizeof(sender_d->connectedSignals) * 8)
        {
            sender_d->connectedSignals[signal_index >> 5] |= (1 << (signal_index & 0x1f));
        }
    
        return true;
    }

    9、信号的发射

    使用emit发射信号时,实际调用MOC实现的信号函数,信号函数内部调用了QMetaObject::activate()函数。

    // SIGNAL 0,ageChanged信号的实现
    void Object::ageChanged(int _t1)
    {
        void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
        QMetaObject::activate(this, &staticMetaObject, 0, _a);
    }
    
    // SIGNAL 1  scoreChanged信号的实现
    void Object::scoreChanged(int _t1)
    {
        void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
        QMetaObject::activate(this, &staticMetaObject, 1, _a);
    }
    void QMetaObject::activate(QObject *sender, const QMetaObject *m,
                               int local_signal_index,void **argv)
    {
        int signalOffset;
        int methodOffset;
        computeOffsets(m, &signalOffset, &methodOffset);
    
        int signal_index = signalOffset + local_signal_index;
    
        if (!sender->d_func()->isSignalConnected(signal_index))
            return; // 如果发送的信号没有槽连接,直接返回
    
        if (sender->d_func()->blockSig)
            return;//如果阻塞,直接返回
    
        int signal_absolute_index = methodOffset + local_signal_index;
    
        void *empty_argv[] = { 0 };
        if (qt_signal_spy_callback_set.signal_begin_callback != 0)
        {
            qt_signal_spy_callback_set.signal_begin_callback(sender, signal_absolute_index,
                                                             argv ? argv : empty_argv);
        }
    
        Qt::HANDLE currentThreadId = QThread::currentThreadId();
    
        QMutexLocker locker(signalSlotLock(sender));
        //获取发送者的连接链表容器
        QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
        if (!connectionLists)
        {
            locker.unlock();
            if (qt_signal_spy_callback_set.signal_end_callback != 0)
                qt_signal_spy_callback_set.signal_end_callback(sender, signal_absolute_index);
            return;
        }
        ++connectionLists->inUse;
    
        //从发送者的连接链表容器中使用信号索引作为索引,获取相应的连接链表
        const QObjectPrivate::ConnectionList *list;
        if (signal_index < connectionLists->count())
            list = &connectionLists->at(signal_index);
        else
            list = &connectionLists->allsignals;
    
        do {
            //索取发送的信号的连接链表的第一个连接
            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;
    
            do
            {
                if (!c->receiver)
                    continue;//如果连接的接收者为空,继续
    
                QObject * const receiver = c->receiver;
                const bool receiverInSameThread = 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))
                {
                    queued_activate(sender, signal_absolute_index, c, argv ? argv : empty_argv);
                    continue;
    #ifndef QT_NO_THREAD
                }
                //阻塞队列连接类型
                else if (c->connectionType == Qt::BlockingQueuedConnection)
                {
                    locker.unlock();
                    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;
                    QCoreApplication::postEvent(receiver, new QMetaCallEvent(c->method_offset, c->method_relative,
                                                                             c->callFunction,
                                                                             sender, signal_absolute_index,
                                                                             0, 0,
                                                                             argv ? argv : empty_argv,
                                                                             &semaphore));
                    semaphore.acquire();
                    locker.relock();
                    continue;
    #endif
                }
    
                QObjectPrivate::Sender currentSender;
                QObjectPrivate::Sender *previousSender = 0;
                if (receiverInSameThread)
                {
                    currentSender.sender = sender;
                    currentSender.signal = signal_absolute_index;
                    currentSender.ref = 1;
                    previousSender = QObjectPrivate::setCurrentSender(receiver, ¤tSender);
                }
                //获取连接的回调函数指针
                const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
                const int method_relative = c->method_relative;
                //如果连接的方法的偏移小于接收者的元对象的方法的偏移
                if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset())
                {
                    //we compare the vtable to make sure we are not in the destructor of the object.
                    locker.unlock();
                    if (qt_signal_spy_callback_set.slot_begin_callback != 0)
                        qt_signal_spy_callback_set.slot_begin_callback(receiver, c->method(), argv ? argv : empty_argv);
                    //根据接收者的方法偏移,接收者等参数调用qt_static_metacall回调函数
                    callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
    
                    if (qt_signal_spy_callback_set.slot_end_callback != 0)
                        qt_signal_spy_callback_set.slot_end_callback(receiver, c->method());
                    locker.relock();
                }
                else
                {
                    const int method = method_relative + c->method_offset;
                    locker.unlock();
    
                    if (qt_signal_spy_callback_set.slot_begin_callback != 0)
                    {
                        qt_signal_spy_callback_set.slot_begin_callback(receiver,
                                                                       method,
                                                                       argv ? argv : empty_argv);
                    }
                    //根据接收者、接收者的方法索引等参数调用发送元对象的metacall
                    metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
    
                    if (qt_signal_spy_callback_set.slot_end_callback != 0)
                        qt_signal_spy_callback_set.slot_end_callback(receiver, method);
    
                    locker.relock();
                }
    
                if (receiverInSameThread)
                    QObjectPrivate::resetCurrentSender(receiver, ¤tSender, previousSender);
    
                if (connectionLists->orphaned)
                    break;
            } while (c != last && (c = c->nextConnectionList) != 0);
    
            if (connectionLists->orphaned)
                break;
        } while (list != &connectionLists->allsignals &&
                 //start over for all signals;
                 ((list = &connectionLists->allsignals), true));
    
        --connectionLists->inUse;
        Q_ASSERT(connectionLists->inUse >= 0);
        if (connectionLists->orphaned)
        {
            if (!connectionLists->inUse)
                delete connectionLists;
        }
        else if (connectionLists->dirty)
        {
            sender->d_func()->cleanConnectionLists();
        }
    
        locker.unlock();
    
        if (qt_signal_spy_callback_set.signal_end_callback != 0)
            qt_signal_spy_callback_set.signal_end_callback(sender, signal_absolute_index);
    
    }
    metacall函数内部调用了qt_metacall函数。
    int QMetaObject::metacall(QObject *object, Call cl, int idx, void **argv)
    {
        if (QMetaObject *mo = object->d_ptr->metaObject)
            return static_cast<QAbstractDynamicMetaObject*>(mo)->metaCall(cl, idx, argv);
        else
            return object->qt_metacall(cl, idx, argv);
    }
    int Object::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
    {
        _id = QObject::qt_metacall(_c, _id, _a);
        if (_id < 0)
            return _id;
        if (_c == QMetaObject::InvokeMetaMethod) 
        {
            if (_id < 4)
                qt_static_metacall(this, _c, _id, _a);
            _id -= 4;
        }
    #ifndef QT_NO_PROPERTIES
          else if (_c == QMetaObject::ReadProperty) 
        {
            void *_v = _a[0];
            switch (_id) {
            case 0: *reinterpret_cast< int*>(_v) = age(); break;
            case 1: *reinterpret_cast< int*>(_v) = score(); break;
            case 2: *reinterpret_cast< Level*>(_v) = level(); break;
            }
            _id -= 3;
        } 
        else if (_c == QMetaObject::WriteProperty) 
        {
            void *_v = _a[0];
            switch (_id) {
            case 0: setAge(*reinterpret_cast< int*>(_v)); break;
            case 1: setScore(*reinterpret_cast< int*>(_v)); break;
            case 2: setLevel(*reinterpret_cast< Level*>(_v)); break;
            }
            _id -= 3;
        } else if (_c == QMetaObject::ResetProperty) {
            _id -= 3;
        } else if (_c == QMetaObject::QueryPropertyDesignable) {
            _id -= 3;
        } else if (_c == QMetaObject::QueryPropertyScriptable) {
            _id -= 3;
        } else if (_c == QMetaObject::QueryPropertyStored) {
            _id -= 3;
        } else if (_c == QMetaObject::QueryPropertyEditable) {
            _id -= 3;
        } else if (_c == QMetaObject::QueryPropertyUser) {
            _id -= 3;
        }
    #endif // QT_NO_PROPERTIES
        return _id;
    }

    qt_metacall函数内部调用了qt_static_metacall函数。

    10、槽函数的调用

    槽函数最终通过qt_static_metacall函数根据参数调用相应的槽函数。

    void Object::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
    {
        if (_c == QMetaObject::InvokeMetaMethod) 
        {
            Q_ASSERT(staticMetaObject.cast(_o));
            Object *_t = static_cast<Object *>(_o);
            switch (_id) {
            case 0: _t->ageChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            case 1: _t->scoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            case 2: _t->onAgeChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            case 3: _t->onScoreChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
            default: ;
            }
        }
    }

    11、函数调用流程分析

    在onAgeChanged(int age)槽函数内部断点调试。
    Qt高级——Qt信号槽机制源码解析
    得到的函数调用栈如下:
    函数调用栈分析:
    Qt高级——Qt信号槽机制源码解析
    Object::qt_metacall函数内部调用了Object::setAge函数,setAge内部调用Object::ageChanged信号函数,ageChanged信号函数内部调用了QMetaObject::activate函数,activate函数内部调用Object::qt_static_metacall函数,最终qt_static_metacall函数内部调用了槽函数onAgeChanged。
    因此在本例中,当调用ob.setProperty("age", QVariant(30));设置属性时,触发了QMetaProperty::Write函数的调用,进而调用MOC实现的moc_Object.cpp文件中的Object::qt_metacall,qt_metacall内部调用setAge函数,setAge函数内部发射信号ageChanged,即调用Object::ageChanged信号函数,Object::ageChanged函数内部调用了Object对象的元对象的QMetaObject::activate函数,activate函数内部调用了Object::qt_static_metacall函数,最终qt_static_metacall内部实现对槽函数onAgeChanged的调用。
    本例中,信号和槽处于同一线程,连接类型为直接连接,因此属于同步调用,是最简单的调用类型。QMetaObject::activate函数内部实际上根据Object对象的元对象中的信号连接链表容器查找得到信号对应的:qt_static_metacall回调函数,进而回调的。

    三、信号槽的标准C++实现

    1、Qt元对象的模拟实现

    Object类的实现:

    #ifndef OBJECT_H
    #define OBJECT_H
    
    #include<map>
    #include <iostream>
    #include <cstring>
    using namespace std;
    
    //宏定义
    #define SLOT(a)     #a
    #define SIGNAL(a)   #a
    #define cpp_slots
    #define cpp_signals protected
    #define cpp_emit
    
    class Object;
    //元对象系统,负责搜集信号与槽的名称
    struct MetaObject
    {
        //信号组
        const char * signal;
        //槽组
        const char * slot;
        //激活某个信号,idx为信号索引
        static void active(Object * sender, int idx);
    };
    
    //被连接对象信息
    struct Connection
    {
        Object * receiver;//信号的接收者
        int method;//槽函数索引
    };
    
    //保存信号索引与连接对象映射
    typedef std::multimap<int, Connection> ConnectionMap;
    typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
    
    //信号和槽的索引查找函数,返回信号或槽的索引
    static int find_string(const char * str, const char * substr)
    {
        if (strlen(str) < strlen(substr))
            return -1;
        int idx = 0;
        int len = strlen(substr);
        bool start = true;
        const char * pos = str;
        while (*pos)
        {
            if (start && !strncmp(pos, substr, len) && pos[len] == '
    ')
                return idx;
            start = false;
            if (*pos == '/n')
            {
                idx++;
                start = true;
            }
            pos++;
        }
        return -1;
    }
    
    class Object
    {
        static MetaObject meta;//静态元对象声明
        void metacall(int idx);//声明元方法调用函数
    public:
        Object()
        {
        }
        //建立连接
        static void cpp_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
        {
            cout << "connecting a signal to slot..." << endl;
            //从元对象数据表中查看信号和槽是否存在
            int sig_idx = find_string(sender->meta.signal, sig);
            int slt_idx = find_string(receiver->meta.slot, slt);
            //如果没有找到信号或者槽
            if (sig_idx == -1 || slt_idx == -1)
            {
                perror("signal or slot not found!");
            }
            else
            {
                //创建一个连接,连接内存储接收者和槽函数的索引
                Connection c = { receiver, slt_idx };
                cout << "add a signal index and an Connection of receiver to sender's Connection map..." << endl;
                //将信号的索引和接收者的信息存储到信号发射者的map容器中
                sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
                cout << "connected success." << endl;
            }
        }
        void emitSignal()//公有测试函数,发送一个信号
        {
            cout << "emiting a signal..." << endl;
            cpp_emit valueChanged();
        }
    cpp_signals:
        void valueChanged();//信号声明
    public cpp_slots:
        void onValueChanged()//槽函数
        {
            cout << "Value Changed."<< endl;
        }
        friend class MetaObject;
    private:
        ConnectionMap connections;//连接键值对
    };
    
    #endif // OBJECT_H

    moc_Object.cpp实现:

    #include "Object.h"
    
    //信号的名称
    static const char signalNames[] = "valueChanged
    ";
    //槽的名称
    static const char slotNames[] = "onValueChanged
    ";
    //静态元对象的填充
    MetaObject Object::meta = { signalNames, slotNames };
    
    //元方法调用函数的实现,根据连接的索引回调槽函数
    void Object::metacall(int idx)
    {
        switch (idx) {
        case 0:
            onValueChanged();
            break;
        default:
            break;
        };
    }
    
    //信号的实现
    void Object::valueChanged()
    {
        MetaObject::active(this, 0);
    }
    
    //激活信号
    void MetaObject::active(Object* sender, int idx)
    {
        ConnectionMapIt it;
        std::pair<ConnectionMapIt, ConnectionMapIt> ret;
        ret = sender->connections.equal_range(idx);
        for (it = ret.first; it != ret.second; ++it)
        {
            Connection c = (*it).second;
            c.receiver->metacall(c.method);//根据索引调用元方法
        }
    }

    2、信号槽模拟使用

    Main.cpp文件:

    #include <iostream>
    #include "Object.h"
    
    using namespace std;
    
    int main(int argc, char *argv[])
    {
        char p[32] = SLOT(Object);
        cout << "cur_value: " << p << endl;
        Object obj1, obj2;
        //连接信号和槽
        Object::cpp_connect(&obj1, SLOT(valueChanged), &obj2, SIGNAL(onValueChanged));
        //发射一个信号进行测试
        obj1.emitSignal();
        getchar();
        return 0;
    }

    四、信号槽的开源实现

    1、sigslot

    sigslot是信号槽的一个非常精炼的C++实现,作者是Sarah Thompson,sigslot实现只有一个头文件sigslot.h,跨平台且线程安全。在WebRTC中,sigslot .h是其基础的事件处理框架, 在多个模块的消息通知,响应处理中被使用。
    sigslot库官网:
    http://sigslot.sourceforge.net/

    Sigslot使用示例如下:

    #include "sigslot.h"
    #include <string>
    #include <stdio.h>
    #include <iostream>
    #include <windows.h>
    
    using namespace sigslot;
    using namespace std;
    
    class CSender
    {
    public:
        sigslot::signal2<string, int> m_pfnsigDanger;
    
        void Panic()
        {
            static int nVal = 0;
            char szVal[20] = { 0 };
            sprintf_s(szVal,20, "help--%d", nVal);
            m_pfnsigDanger(szVal, nVal++);
        }
    };
    
    class CReceiver :public sigslot::has_slots<>
    {
    public:
        void OnDanger(string strMsg, int nVal)
        {
            //printf("%s ==> %d", strMsg.c_str(), nVal);
            cout << strMsg.c_str() << " ==> " << nVal << endl;
        }
    };
    
    int main()
    {
        CSender sender;
        CReceiver recever;
        cout << "create object ok..." << endl;  
        sender.m_pfnsigDanger.connect(&recever, &CReceiver::OnDanger); 
        cout << "connect succ!" << endl;
        while (1)
        {
            cout << "in while..." << endl;
            sender.Panic();
            Sleep(2000);
            cout << "end of sleep" << endl;
        }
        return 0;
    }

    如果在Qt工程中使用sigslot.h,sigslot.h中的emit函数名会和Qt中的emit宏冲突,修改方法有两个,一是将sigslot.h的emit改成其他名字,二是在.pro文件中添加DEFINES+=QT_NO_EMIT,禁用Qt的emit宏。

    2、Boost.Signals

    Boost.Signals实现了signals/slots模式,信号(signals)被发射,而插槽(slots)接收该信号。

    #include <iostream>
    #include "boost/signals.hpp"
    
    void firstSlot() {
      std::cout << "void firstSlot()";
    }
    
    class secondSlot {
    public:
      void operator()() const {
        std::cout <<
          "void secondSlot::operator()() const ";
      }
    };
    
    int main() 
    {
      boost::signal<void ()> sig;
      sig.connect(&firstSlot);
      sig.connect(secondSlot());
    
      std::cout << "Emitting a signal... ";
      sig();
    }

    插槽函数的执行顺序是随机的,可以使用分组参数来控制调用顺序。
    sig.connect(1,&firstSlot);
    sig.connect(2,secondSlot());

    3、Qt信号槽实现与Boost信号槽实现的区别

    Boost.SignalsQt Signals 和 Slots
    一个信号就是一个对象 信号只能是成员函数
    发出信号类似于函数调用 发出信号类似于函数调用,Qt 提供了一个 emit 关键字来完成这个操作
    信号可以是全局的、局部的或者是成员对象 信号只能是成员函数
    任何能够访问到信号对象的代码都可以发出信号 只有信号的拥有者才能发出信号
    槽是任何可被调用的函数或者函数对象 槽是经过特别设计的成员函数
    可以有返回值,返回值可以在多个槽中使用 没有返回值
    同步的 同步的或者队列的
    非线程安全 线程安全,可以跨线程使用
    当且仅当槽是可追踪的时候,槽被销毁时,连接自动断开 槽被销毁时,连接都会自动断开(因为所有槽都是可追踪的)
    类型安全(编译器检查) 类型安全(运行期检查)
    参数列表必须完全一致 槽可以忽略信号中多余的参数
    信号、槽可以是模板 信号、槽不能是模板
    C++ 直接实现   通过由 moc 生成的元对象实现(moc 以及元对象系统都是 C++ 直接实现的)
    没有内省机制 可以通过内省发现,可以通过元对象调用,

    ============= End

  • 相关阅读:
    [问题2015S13] 复旦高等代数 II(14级)每周一题(第十四教学周)
    [问题2015S12] 复旦高等代数 II(14级)每周一题(第十三教学周)
    [问题2015S11] 复旦高等代数 II(14级)每周一题(第十二教学周)
    [问题2015S10] 复旦高等代数 II(14级)每周一题(第十一教学周)
    [问题2015S09] 复旦高等代数 II(14级)每周一题(第十教学周)
    [问题2015S08] 复旦高等代数 II(14级)每周一题(第九教学周)
    @JsonFormat与@DateTimeFormat注解的使用
    intellij idea 重命名或复制一个项目(不用重启)
    PageHelper中默认PageInfo成员变量
    图片的base64编码
  • 原文地址:https://www.cnblogs.com/lsgxeva/p/12639283.html
Copyright © 2011-2022 走看看