QCoreApplication QMetaCallEvent 压缩
每个排队的槽调用最终都会将QMetaCallEvent 发送到目标对象。该事件包含发送者对象、信号 id、槽索引和打包的调用参数。在 Qt 5 上,信号 id 通常不等于 QMetaObject::signalIndex() 返回的值:它是一个计算的索引,就好像对象只有信号方法而没有其他方法一样。
目标是压缩此类调用,以便事件队列中对于给定的(发送者对象、发送者信号、接收者对象、接收者槽)元组仅存在一个唯一调用。
这是唯一合理的方法,无需更改源或目标对象,同时保持最小的开销。我的其他答案中的事件循环递归方法每个事件都有严重的堆栈开销,当 Qt 是为 64 位指针架构构建时,大约为 1kbyte。
当新事件发布到一个对象已经发布了一个或多个事件时,可以访问事件队列。在这种情况下,QCoreApplication::postEvent 调用 QCoreApplication::compressEvent。当第一个事件发布到对象时,不会调用 compressEvent。在此方法的重新实现中,可以检查发布到目标对象的QMetaCallEvent 的内容是否有对您的槽的调用,并且必须删除过时的副本。必须包含私有 Qt 标头才能获得QMetaCallEvent、QPostEvent 和QPostEventList 的定义。
优点:发送者和接收者对象都不必知道任何事情。信号和槽按原样工作,包括 Qt 5 中的方法指针调用。Qt 本身使用这种压缩事件的方式。
缺点:需要包含私有 Qt 标头并强制清除 QEvent::posted 标志。
不用破解QEvent::posted 标志,要删除的事件可以在一个单独的列表中排队,并在触发零持续时间计时器时在compressEvent 调用之外删除。这会产生额外的事件列表以及每个 event deletion iterating through the posted event list 的开销。
其他方法
采用其他方式的目的是不使用 Qt 的内部结构。
L1 第一个限制是不能访问私有定义的QMetaCallEvent 的内容。可以这样处理:
可以在源对象和目标对象之间连接具有与目标相同签名的信号和槽的代理对象。
在代理对象上运行 QMetaCallEvent 允许提取调用类型、被调用槽 id 和参数。
可以代替信号槽连接,将事件显式发布到目标对象。目标对象或事件过滤器必须从事件数据中显式重新合成槽调用。
可以使用自定义 compressedConnect 实现来代替 QObject::connect。这充分暴露了信号和槽的细节。代理对象可用于在发送者对象一侧执行与 queued_activate 的压缩友好等效。
L2 第二个限制是不能完全重新实现QCoreApplication::compressEvent,因为事件列表是私有定义的。我们仍然可以访问被压缩的事件,我们仍然可以决定是否删除它,但是没有办法迭代事件列表。因此:
-
可以通过从notify(因此也可以从eventFilter()、event() 或插槽)递归调用sendPostedEvents 来隐式访问事件队列。这不会导致死锁,因为QCoreApplication::sendPostedEvents 在通过sendEvent 传递事件时不能(也不会)持有事件循环互斥锁。可以按如下方式过滤事件:
- 在全球范围内重新实现
QCoreApplication::notify,
- 在全球范围内通过注册
QInternal::EventNotifyCallback,
- 在本地通过将事件过滤器附加到对象,
- 通过在目标类中重新实现
QObject::event() 在本地显式地进行。
重复的事件仍会发布到事件队列中。从sendPostedEvents 内部对notify 的递归调用会消耗相当多的堆栈空间(在 64 位指针架构上预算为 1kb)。
-
在向对象发布新事件之前,可以通过调用QCoreApplication::removePostedEvents 来删除已经存在的事件。不幸的是,在QCoreApplication::compressEvent 中执行此操作会导致死锁,因为事件队列互斥锁已被持有。
包含指向接收者对象的指针的自定义事件类可以在构造函数中自动调用removePostedEvents。
-
可以重新占用现有的压缩事件,例如QEvent::Exit。
这些事件的集合是一个实现细节,可能会发生变化。除了接收者QObject 指针之外,Qt 不会区分这些事件。实现需要每个(事件类型,接收对象)元组的代理 QObject 开销。
实施
下面的代码适用于 Qt 4 和 Qt 5。对于后者,请确保将 QT += core-private 添加到您的 qmake 项目文件中,以便包含私有 Qt 标头。
其他答案中给出了不使用 Qt 内部标头的实现:
有两个事件删除代码路径,由if (true) 选择。启用的代码路径通常会保留最近的事件并且最有意义。或者,您可能希望保留最旧的事件 - 这就是禁用代码路径的作用。
#include <QApplication>
#include <QMap>
#include <QSet>
#include <QMetaMethod>
#include <QMetaObject>
#include <private/qcoreapplication_p.h>
#include <private/qthread_p.h>
#include <private/qobject_p.h>
#include <QWidget>
#include <QPushButton>
#include <QPlainTextEdit>
#include <QSpinBox>
#include <QFormLayout>
// Works on both Qt 4 and Qt 5.
//
// Common Code
/*! Keeps a list of singal indices for one or more meatobject classes.
* The indices are signal indices as given by QMetaCallEvent.signalId.
* On Qt 5, those do *not* match QMetaObject::methodIndex since they
* exclude non-signal methods. */
class SignalList {
Q_DISABLE_COPY(SignalList)
typedef QMap<const QMetaObject *, QSet<int> > T;
T m_data;
/*! Returns a signal index that is can be compared to QMetaCallEvent.signalId. */
static int signalIndex(const QMetaMethod & method) {
Q_ASSERT(method.methodType() == QMetaMethod::Signal);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
int index = -1;
const QMetaObject * mobj = method.enclosingMetaObject();
for (int i = 0; i <= method.methodIndex(); ++i) {
if (mobj->method(i).methodType() != QMetaMethod::Signal) continue;
++ index;
}
return index;
#else
return method.methodIndex();
#endif
}
public:
SignalList() {}
void add(const QMetaMethod & method) {
m_data[method.enclosingMetaObject()].insert(signalIndex(method));
}
void remove(const QMetaMethod & method) {
T::iterator it = m_data.find(method.enclosingMetaObject());
if (it != m_data.end()) {
it->remove(signalIndex(method));
if (it->empty()) m_data.erase(it);
}
}
bool contains(const QMetaObject * metaObject, int signalId) {
T::const_iterator it = m_data.find(metaObject);
return it != m_data.end() && it.value().contains(signalId);
}
};
//
// Implementation Using Event Compression With Access to Private Qt Headers
struct EventHelper : private QEvent {
static void clearPostedFlag(QEvent * ev) {
(&static_cast<EventHelper*>(ev)->t)[1] &= ~0x8001; // Hack to clear QEvent::posted
}
};
template <class Base> class CompressorApplication : public Base {
SignalList m_compressedSignals;
public:
CompressorApplication(int & argc, char ** argv) : Base(argc, argv) {}
void addCompressedSignal(const QMetaMethod & method) { m_compressedSignals.add(method); }
void removeCompressedSignal(const QMetaMethod & method) { m_compressedSignals.remove(method); }
protected:
bool compressEvent(QEvent *event, QObject *receiver, QPostEventList *postedEvents) {
if (event->type() != QEvent::MetaCall)
return Base::compressEvent(event, receiver, postedEvents);
QMetaCallEvent *mce = static_cast<QMetaCallEvent*>(event);
if (! m_compressedSignals.contains(mce->sender()->metaObject(), mce->signalId())) return false;
for (QPostEventList::iterator it = postedEvents->begin(); it != postedEvents->end(); ++it) {
QPostEvent &cur = *it;
if (cur.receiver != receiver || cur.event == 0 || cur.event->type() != event->type())
continue;
QMetaCallEvent *cur_mce = static_cast<QMetaCallEvent*>(cur.event);
if (cur_mce->sender() != mce->sender() || cur_mce->signalId() != mce->signalId() ||
cur_mce->id() != mce->id())
continue;
if (true) {
/* Keep The Newest Call */
// We can't merely qSwap the existing posted event with the new one, since QEvent
// keeps track of whether it has been posted. Deletion of a formerly posted event
// takes the posted event list mutex and does a useless search of the posted event
// list upon deletion. We thus clear the QEvent::posted flag before deletion.
EventHelper::clearPostedFlag(cur.event);
delete cur.event;
cur.event = event;
} else {
/* Keep the Oldest Call */
delete event;
}
return true;
}
return false;
}
};
//
// Demo GUI
class Signaller : public QObject {
Q_OBJECT
public:
Q_SIGNAL void emptySignal();
Q_SIGNAL void dataSignal(int);
};
class Widget : public QWidget {
Q_OBJECT
QPlainTextEdit * m_edit;
QSpinBox * m_count;
Signaller m_signaller;
Q_SLOT void emptySlot() {
m_edit->appendPlainText("emptySlot invoked");
}
Q_SLOT void dataSlot(int n) {
m_edit->appendPlainText(QString("dataSlot(%1) invoked").arg(n));
}
Q_SLOT void sendSignals() {
m_edit->appendPlainText(QString("\nEmitting %1 signals").arg(m_count->value()));
for (int i = 0; i < m_count->value(); ++ i) {
emit m_signaller.emptySignal();
emit m_signaller.dataSignal(i + 1);
}
}
public:
Widget(QWidget * parent = 0) : QWidget(parent),
m_edit(new QPlainTextEdit), m_count(new QSpinBox)
{
QFormLayout * l = new QFormLayout(this);
QPushButton * invoke = new QPushButton("Invoke");
m_edit->setReadOnly(true);
m_count->setRange(1, 1000);
l->addRow("Number of slot invocations", m_count);
l->addRow(invoke);
l->addRow(m_edit);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
connect(invoke, &QPushButton::clicked, this, &Widget::sendSignals);
connect(&m_signaller, &Signaller::emptySignal, this, &Widget::emptySlot, Qt::QueuedConnection);
connect(&m_signaller, &Signaller::dataSignal, this, &Widget::dataSlot, Qt::QueuedConnection);
#else
connect(invoke, SIGNAL(clicked()), SLOT(sendSignals()));
connect(&m_signaller, SIGNAL(emptySignal()), SLOT(emptySlot()), Qt::QueuedConnection);
connect(&m_signaller, SIGNAL(dataSignal(int)), SLOT(dataSlot(int)), Qt::QueuedConnection);
#endif
}
};
int main(int argc, char *argv[])
{
CompressorApplication<QApplication> a(argc, argv);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
a.addCompressedSignal(QMetaMethod::fromSignal(&Signaller::emptySignal));
a.addCompressedSignal(QMetaMethod::fromSignal(&Signaller::dataSignal));
#else
a.addCompressedSignal(Signaller::staticMetaObject.method(Signaller::staticMetaObject.indexOfSignal("emptySignal()")));
a.addCompressedSignal(Signaller::staticMetaObject.method(Signaller::staticMetaObject.indexOfSignal("dataSignal(int)")));
#endif
Widget w;
w.show();
return a.exec();
}
#include "main.moc"