Qt源码阅读(四) 事件循环

事件系统

文章为本人理解,如有理解不到位之处,烦请各位指正。‍♂️

文章目录

  • 事件系统
    • 什么是事件循环?
    • 事件是如何产生的?
    • 事件是如何处理的?
      • sendEvent
      • postEvent
    • 事件循环是怎么遍历的?
    • 事件过滤器
    • 夹带私货时间

Qt的事件循环是所有Qt开发者都无法避免的一个重要概念。因此,本篇博客将介绍Qt源码中与事件循环相关的部分,帮助读者更好地理解Qt事件循环的机制。

在深入源码之前,先抛出几个问题。随后,我们将通过源码,逐一解析,揭开事件循环的面纱。

  1. 事件循环是什么?
  2. 事件是怎么产生的?
  3. 事件是如何处理的?

什么是事件循环?

在ChatGPT上搜索到的概念是:

在Qt中,事件循环是一种机制,用于处理各种异步事件。事件循环通过一个事件队列来管理和调度事件,当队列中有事件时,事件循环会从队列中依次取出事件并处理,直到队列为空或者事件循环被中断

事件的产生可以是用户输入、系统信号、网络请求、定时器等,Qt提供了一系列的事件处理函数和信号槽机制,可以方便地将这些事件与具体的操作相绑定。

因此,Qt的事件循环机制是Qt应用程序实现异步响应和多线程编程的基础。

个人理解,Qt的事件循环是通过一个队列来循环处理事件的机制。当队列中有事件时,事件循环会处理事件;如果队列中没有事件,则事件循环会阻塞等待。

事件是如何产生的?

事件的产生可以分为两种:

  1. 程序外部产生:指系统产生的事件,例如鼠标按下(MouseButtonPress)、按键按下(KeyPress)等。Qt通过捕捉系统事件,将其封装成自己的QEvent类,再将事件发送出去。

  2. 程序内部产生:指在代码中手动创建一个事件,然后通过sendEvent/postEvent将事件发送到事件循环中。其中,sendEvent阻塞型的发送方式,会等待事件处理完成后再继续执行;而postEvent非阻塞型的发送方式,会将事件放入事件队列中,并立即返回

事件是如何处理的?

让我们通过一个流程图简单了解事件从发出到处理的过程。
在接下来的解析中,我们将通过分析源代码,逐步验证流程图中的每个步骤。请各位读者耐心继续往下阅读
Qt源码阅读(四) 事件循环_第1张图片

下面我们将通过分析源码,来揭秘为什么sendEvent/postEvent是阻塞和非阻塞的、以及事件的处理流程

sendEvent

首先,我们看sendEvent

bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
{
	// sendEvent是阻塞调用
    Q_TRACE(QCoreApplication_sendEvent, receiver, event, event->type());

    if (event)
        event->spont = false;
    return notifyInternal2(receiver, event);
}

可以看到,sendEvent是调用了notifyInternal2这个函数

bool QCoreApplication::notifyInternal2(QObject *receiver, QEvent *event)
{
	...
    // Qt enforces the rule that events can only be sent to objects in
    // the current thread, so receiver->d_func()->threadData is
    // equivalent to QThreadData::current(), just without the function
    // call overhead.
    // 事件只能在同一个线程被send
    QObjectPrivate *d = receiver->d_func();
    QThreadData *threadData = d->threadData;
    QScopedScopeLevelCounter scopeLevelCounter(threadData);
    if (!selfRequired)
        return doNotify(receiver, event);
    return self->notify(receiver, event);
}

进一步跟踪到其doNotify函数

static bool doNotify(QObject *receiver, QEvent *event)
{
    if (receiver == nullptr) {                        // serious error
        qWarning("QCoreApplication::notify: Unexpected null receiver");
        return true;
    }

#ifndef QT_NO_DEBUG
	// 检查接受线程与当前是否同线程
    QCoreApplicationPrivate::checkReceiverThread(receiver);
#endif

	// QWidget类必须用QApplication
    return receiver->isWidgetType() ? false : QCoreApplicationPrivate::notify_helper(receiver, event);
}

最后到了咱们这次分析的第一个重点——事件通知QCoreApplicationPrivate::notify_helper

bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
{
    // ...
}

通过分析这个函数,我们就可以大致知道事件处理的几个流程:

  1. 判断QCoreApplication有没有安装事件过滤器,有就把信号发送到QCoreApplication所安装事件过滤器里,由事件过滤器对事件进行处理。
// send to all application event filters (only does anything in the main thread)
if (QCoreApplication::self
    && receiver->d_func()->threadData.loadRelaxed()->thread.loadAcquire() == mainThread()
    && QCoreApplication::self->d_func()->sendThroughApplicationEventFilters(receiver, event)) {
    filtered = true;
    return filtered;
}
  1. 如果QCoreApplication没有安装事件过滤器或者所安装的事件过滤器不处理,则判断事件接受对象有没有安装事件过滤器,有就将事件发送到事件过滤器去处理。
// send to all receiver event filters
if (sendThroughObjectEventFilters(receiver, event)) {
    filtered = true;
    return filtered;
}

事件过滤器的具体分析,在后面会分析,现在我们先关注主要的流程。

  1. 同样,如果事件过滤器不进行处理,则直接调用事件接受对象的**event**函数进行处理。因为是直接调用的对象的event,所以sendEvent函数是阻塞的
    // deliver the event
    // 直接调用对象的event函数,所以是阻塞的
    consumed = receiver->event(event);
    return consumed;

postEvent


在下面的分析过程中,我们将通过对postEvent函数的分析,来解释为什么这个函数是非阻塞的

让我们对postEvent函数进行分析,以便解密其工作原理。

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
    // ...
}
  1. 首先判断事件接收对象是否为空
// 事件的接收者不能为空
if (receiver == nullptr) {
    qWarning("QCoreApplication::postEvent: Unexpected null receiver");
    delete event;
    return;
}
  1. 将事件接收对象所在线程的post事件列表上锁,如果已经被锁了,就把事件删除掉,并返回,防止泄露。
// 对事件接受对象所在线程的事件处理列表上锁
auto locker = QCoreApplicationPrivate::lockThreadPostEventList(receiver);
if (!locker.threadData) {
    // posting during destruction? just delete the event to prevent a leak
    delete event;
    return;
}
  1. 将一些可以压缩的事件进行压缩,及多个事件压缩成只推送最后的一个事件。Qt界面的update就是这个操作,为了防止多次刷新导致卡顿,短时间内多次的调用update可能只会刷新一次
// if this is one of the compressible events, do compression
// 将重复的事件,进行压缩
if (receiver->d_func()->postedEvents
    && self && self->compressEvent(event, receiver, &data->postEventList)) {
    Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
    return;
}
  1. 将事件插入接收对象所在线程的post事件列表中,并唤醒线程的事件调度器,来进行事件的处理。所以postEvent是非阻塞的,因为其只是把事件插入了线程的事件列表,唤醒事件调度器之后便返回
    // ...

	// 将事件加入事件队列
    data->postEventList.addEvent(QPostEvent(receiver, event, priority));
    eventDeleter.take();
    event->posted = true;
    ++receiver->d_func()->postedEvents;
    data->canWait = false;
    locker.unlock();

	// 唤醒事件分发器
    QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();
    if (dispatcher)
        dispatcher->wakeUp();

事件循环是怎么遍历的?


在上面的内容中,我们只是通过跟随sendEvent来了解了事件处理的流程,但是postEvent将事件插入事件队列中,事件循环又是如何去循环运转的呢?

让我们从常见的return a.exec()开始,深入源码,详细了解事件循环的运行机制吧‍️!

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.show();
    return a.exec();
}

上面是一个经典的QtGUI程序的main函数,调用了a.exec()


int QCoreApplication::exec()
{
    ...
    
    threadData->quitNow = false;
    QEventLoop eventLoop;
    self->d_func()->in_exec = true;
    self->d_func()->aboutToQuitEmitted = false;
    int returnCode = eventLoop.exec();
    
    ...
}

而看QApplication::exec的源码,实际上就是开启了一个事件循环QEventLoop)。同样,我们去看QEventLoop::exec的源码,进一步了解处理事件队列的步骤是什么。

int QEventLoop::exec(ProcessEventsFlags flags)
{
    // ...

    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);

    ref.exceptionCaught = false;
    return d->returnCode.loadRelaxed();
}

上面可以看到,QEvenLoop::exec里,是一个while循环,循环的去调用processEvent,而且设置了WaitForMoreEvents就是说,如果没有事件,就阻塞等待。


void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags, int ms)
{
    QThreadData *data = QThreadData::current();
    if (!data->hasEventDispatcher())
        return;
    QElapsedTimer start;
    start.start();
    while (data->eventDispatcher.loadRelaxed()->processEvents(flags & ~QEventLoop::WaitForMoreEvents)) {
        if (start.elapsed() > ms)
            break;
    }
}

阅读processEvent,其调用了线程的事件调度器QAbstrctEventDispatcher,而这个类是一个抽象基类,根据不同的平台,有不同的实现,我们以windows下(QEventDispatcherWin32)的为例,接着分析事件处理的流程。

bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
    Q_D(QEventDispatcherWin32);

	...

    // To prevent livelocks, send posted events once per iteration.
    // QCoreApplication::sendPostedEvents() takes care about recursions.
    sendPostedEvents();

    ...
}

void QEventDispatcherWin32::sendPostedEvents()
{
    Q_D(QEventDispatcherWin32);

    if (d->sendPostedEventsTimerId != 0)
        KillTimer(d->internalHwnd, d->sendPostedEventsTimerId);
    d->sendPostedEventsTimerId = 0;

    // Allow posting WM_QT_SENDPOSTEDEVENTS message.
    d->wakeUps.storeRelaxed(0);

    QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData.loadRelaxed());
}

可以看到,事件调度器兜兜转转最终还是调用了QCoreApplicationsendPostEvents

void QCoreApplicationPrivate::sendPostedEvents(QObject *receiver, int event_type,
                                               QThreadData *data)
{
	// ...
    
    if (receiver && receiver->d_func()->threadData != data) {
        qWarning("QCoreApplication::sendPostedEvents: Cannot send "
                 "posted events for objects in another thread");
        return;
    }

    ...


    while (i < data->postEventList.size()) {
       ...

        // first, we diddle the event so that we can deliver
        // it, and that no one will try to touch it later.
        pe.event->posted = false;
        QEvent *e = pe.event;
        QObject * r = pe.receiver;

        --r->d_func()->postedEvents;
        Q_ASSERT(r->d_func()->postedEvents >= 0);

        // next, update the data structure so that we're ready
        // for the next event.
        const_cast<QPostEvent &>(pe).event = nullptr;

        locker.unlock();
        const auto relocker = qScopeGuard([&locker] { locker.lock(); });

        QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)

        // after all that work, it's time to deliver the event.
        QCoreApplication::sendEvent(r, e);

        // careful when adding anything below this point - the
        // sendEvent() call might invalidate any invariants this
        // function depends on.
    }

    cleanup.exceptionCaught = false;
}

我们一个一个的分块分析:

  1. 判断是否在一个线程
if (receiver && receiver->d_func()->threadData != data) {
    qWarning("QCoreApplication::sendPostedEvents: Cannot send "
             "posted events for objects in another thread");
    return;
}

  1. 将事件发送出去(sendEvent)
while (i < data->postEventList.size()) {
       ...

        // first, we diddle the event so that we can deliver
        // it, and that no one will try to touch it later.
        pe.event->posted = false;
        QEvent *e = pe.event;
        QObject * r = pe.receiver;

        --r->d_func()->postedEvents;
        Q_ASSERT(r->d_func()->postedEvents >= 0);

        // next, update the data structure so that we're ready
        // for the next event.
        const_cast<QPostEvent &>(pe).event = nullptr;

        locker.unlock();
        const auto relocker = qScopeGuard([&locker] { locker.lock(); });

        QScopedPointer<QEvent> event_deleter(e); // will delete the event (with the mutex unlocked)

        // after all that work, it's time to deliver the event.
        QCoreApplication::sendEvent(r, e);

        // careful when adding anything below this point - the
        // sendEvent() call might invalidate any invariants this
        // function depends on.
    }


可以看到,核心还是调用sendEvent将事件发送出去,而前面我们对sendEvent的源码分析我们可以看到,事件先是经过事件过滤器,再经过对象的event函数,来进行事件的处理

所以,下面我们将介绍:事件过滤器

事件过滤器

在实际应用中,我们经常要将某一个窗口部件的某个事件如鼠标滑轮滚动拦截,然后执行我们自己想要的操作。这个时候,我们就可以用到事件过滤器(EventFilter**) **

首先,我们需要自己编写一个eventFilter函数,

bool Class::eventFilter(QObject* watcher, QEvent* event)
{
	//以过滤鼠标滚轮事件为例
    if (object == m_watcherObject && event->type() == QEvent::Wheel) {
    	// do something
        return true;       
    }

    QWidget::eventFilter(watcher, event);
}

然后,我们需要为要拦截的某个窗口部件,安装事件过滤器

void Class::initUI() 
{
	QWidget* m_watcherObject = new QWidget(this);
    // 为对象安装一个事件过滤器
	m_watcherObject->installEventFilterr(this);
}

initUI();

那么一个对象安装的多个事件过滤器,会以什么样的顺序触发呢?我们在帮助文档中可以看到:

后安装的事件过滤器会先触发

这一点,我们可以在源码里得到佐证:

void QObject::installEventFilter(QObject *obj)
{
    Q_D(QObject);
    if (!obj)
        return;
    if (d->threadData != obj->d_func()->threadData) {
        qWarning("QObject::installEventFilter(): Cannot filter events for objects in a different thread.");
        return;
    }

    if (!d->extraData)
        d->extraData = new QObjectPrivate::ExtraData;

    // clean up unused items in the list
    d->extraData->eventFilters.removeAll((QObject*)nullptr);
    d->extraData->eventFilters.removeAll(obj);
    d->extraData->eventFilters.prepend(obj);
}

可以清楚的看到,事件过滤器,是以prepend的形式被添加进事件过滤器列表的。

QCoreApplicationPrivate::sendThroughObjectEventFilters中,则是遍历事件过滤器列表。


    if (receiver != QCoreApplication::instance() && receiver->d_func()->extraData) {
        for (int i = 0; i < receiver->d_func()->extraData->eventFilters.size(); ++i) {
            QObject *obj = receiver->d_func()->extraData->eventFilters.at(i);
            if (!obj)
                continue;
            if (obj->d_func()->threadData != receiver->d_func()->threadData) {
                qWarning("QCoreApplication: Object event filter cannot be in a different thread.");
                continue;
            }
            if (obj->eventFilter(receiver, event))
                return true;
        }
    }
    return false;

那么,当有鼠标滚轮事件触发的时候,我们可以看到sendEvent会先走到事件过滤器里**,如果**eventFilter返回一个true,那么事件就不会被继续派发,否则,将会将事件发送到其他的事件过滤器里进行处理,如果其他的事件过滤器均对该事件不进行处理,那么事件将会继续往下派发,走到事件的处理函数event

夹带私货时间

  1. 之前有说到processEvent,添加一个小经验。当我们有时候不得不在主线程循环执行很耗时的操作的时候,这个时候,界面就会刷新不过来,就会导致界面卡顿,影响使用。但是,我们可以在这个循环里,手动调用qApp->processEvent(),这样就可以手动调用处理掉所有的事件,就可以解决卡顿的问题

创作不易,如果对您有帮助,点赞、关注、收藏支持一下!不甚感激!

你可能感兴趣的:(Qt源码剖析,qt,开发语言)