事件监听器如何工作?


125

在我今天有关Unity的演讲中,我们讨论了通过检查用户是否按下按钮的每一帧来更新播放器位置。有人说这效率低下,我们应该改用事件监听器。

我的问题是,无论使用哪种编程语言或应用哪种情况,事件监听器如何工作?

我的直觉是假定事件侦听器不断检查事件是否已被触发,这意味着,在我的情况下,与检查事件是否被触发的每个帧没有什么不同。

根据课堂讨论,事件监听器似乎以不同的方式工作。

事件监听器如何工作?


34
事件监听器根本不检查。当“监听”事件触发时,将调用该方法。
罗伯特·哈维

13
是的,但是它如何“监听”,不是经常检查吗?
加里(Gary Holiday)

28
不。“事件监听器”可能是一个不好的选择。它实际上根本没有“收听”。事件侦听器所做的只是等待事件触发时触发,这与其他任何方法一样。在以这种方式调用之前,它什么也不做。
罗伯特·哈维

28
每次检查按钮是否被按下时,都需要花费时钟周期。仅当实际按下按钮时,事件处理程序(侦听器)才会使您花费。
罗伯特·哈维

45
@RobertHarvey-不一定,因为“侦听器”仍需要在较低级别进行持续轮询。您只需将复杂性从您自己的代码层更深地推到硬件中断之类的地方。是的,这通常会更有效,但这不是因为侦听优于轮询,而是因为较低级别的轮询比C#和您与硬件之间的15层抽象层的轮询更有效。
DavorŽdralo18年

Answers:


140

与您提供的轮询示例(每帧检查一次按钮)不同,事件侦听器根本不检查按钮是否被按下。而是在按下按钮时调用它。

也许“事件侦听器”一词让您大跌眼镜。这个词表明“听众”正在积极地做听的事情,而实际上却什么也没做。“侦听器”仅仅是订阅该事件的功能或方法。事件触发时,将调用侦听器方法(“事件处理程序”)。

事件模式的好处是在实际按下按钮之前没有任何花费。可以通过这种方式处理事件而无需对其进行监视,因为它起源于我们所谓的“硬件中断”,它短暂地抢占了正在运行的代码以触发事件。

某些UI和游戏框架使用一种称为“消息循环”的方法,该事件将事件排队以便在稍后(通常较短)的时间执行,但是您仍然需要硬件中断才能将该事件首先放入消息循环中。


54
值得一提的是,直到按下按钮才需要付费的原因是按钮是“特殊的”,计算机具有操作系统可以使用的中断和其他特殊功能,这些功能被抽象到用户空间应用程序中。
whatsisname

46
@whatsisname的内幕很深,实际上,游戏引擎可能无法处理中断,但实际上仍在循环中轮询事件源。这只是这个投票是集中和优化,以便增加更多的事件监听器不增加额外的投票和复杂性。
gntskn

7
@PieterGeerkens我猜gntskn意味着作为游戏引擎循环的一部分,有一个步骤可以检查任何未完成的事件。事件将在每个循环以及所有其他每个循环一次的活动中进行处理。不会有单独的循环来检查事件。
约书亚·泰勒

2
@Voo:更多的原因是不要在本文中详细介绍这一级别。
罗伯特·哈维

2
@Voo:我说的是按钮,例如键盘上的物理键和鼠标按钮。
whatsisname

52

事件侦听器类似于电子邮件新闻订阅(您注册自己以接收更新,其发送随后由发件人启动),而不是无休止地刷新网页(您就是其中一个发起信息传递的人)。

使用事件对象实现事件系统,该事件对象管理订户列表。感兴趣的对象(称为“ 订户”,“ 侦听器”,“ 委托 ”等)可以通过调用订阅事件的方法来订阅自己的事件,从而将事件添加到列表中。每当触发事件时(术语还可以包括:被调用触发被调用运行等),它将在每个订阅者上调用适当的方法,以将事件通知他们,并传递他们需要了解的所有上下文信息发生了什么。


38

简短而不能令人满意的答案是,应用程序接收到信号(事件),并且仅在该点调用例程。

较长的解释涉及更多的内容。

客户事件来自哪里?

每个现代应用程序都有一个内部(通常是半隐藏的)“事件循环”,该事件循环将事件分发到应接收事件的正确组件。例如,“单击”事件被发送到按钮,该按钮的表面在当前鼠标坐标处可见。这是最简单的级别。实际上,由于某些事件和某些组件将直接接收消息,因此OS会执行很多此类调度。

应用程序事件来自哪里?

操作系统在事件发生时调度事件。他们通过自己的驾驶员通知来做出反应。

驱动程序如何生成事件?

我不是专家,但是可以肯定有些使用CPU中断:当有新数据可用时,它们控制的硬件会在CPU上加一个脚。CPU触发驱动程序,该驱动程序处理传入的数据,该驱动程序最终生成要分派的事件(队列),然后将控制权返回给OS。

因此,如您所见,您的应用程序并非一直都在运行。随着事件的发生,OS(sorta)会触发一堆程序,但在其余时间中什么都不做。


有一些例外,例如一次游戏可能会产生不同的效果


10
此答案说明了为什么浏览器中不涉及鼠标单击事件的轮询。硬件生成中断=>驱动程序将其解析为OS事件=>浏览器将其解析为DOM事件=> JS引擎为该事件运行侦听器。
Tibos

@Tibos afaict也适用于键盘事件,计时器事件,绘画事件等
。– Sklivvz

19

术语

  • 事件:可能发生的事情。

  • 事件触发事件的特定发生;事件正在发生。

  • 事件监听器:可以监视事件触发的东西。

  • 事件处理程序:事件侦听器检测到事件触发时发生的事情。

  • 事件订户:事件处理程序应该调用的响应。

这些定义不依赖于实现,因此可以以不同的方式实现。

这些术语中的一些通常被误认为是同义词,因为用户通常不需要区分它们。

常见场景

  1. 编程逻辑事件。

    • 事件是某个方法被调用时。

    • 一个激发事件是该方法的特定呼叫。

    • 事件监听器是在被称为上调用该事件处理程序的每个事件触发事件方法的钩。

    • 事件处理程序调用事件的用户的集合。

    • 事件的用户(一个或多个)执行任何动作(S)系统是指在应对事件的发生发生。

  2. 外部事件。

    • 事件是外部事件,可以从可观察到的事件中推断出来。

    • 一个事件触发是当外部发生,可以认定为已经发生。

    • 事件侦听器以某种方式检测到事件的点火,经常通过轮询可观察到的(多个),然后它调用在检测到的事件触发的事件处理程序。

    • 事件处理程序调用事件的用户的集合。

    • 事件的用户(一个或多个)执行任何动作(S)系统是指在应对事件的发生发生。

轮询与将挂钩插入事件的触发机制

其他人指出的是,通常不需要轮询。这是因为可以通过让事件触发自动调用事件处理程序来实现事件侦听器,这通常是在事件为系统级事件时实现事物的最有效方法。

以此类推,如果邮递员敲门并直接将邮件交给您,则无需每天检查邮箱中的邮件。

但是,事件侦听器也可以通过轮询来工作。轮询不一定需要检查特定值或其他可观察的值。它可能更复杂。但是,总的来说,轮询的重点是推断何时发生某个事件以便可以对其进行响应。

以此类推,当邮政工作人员只是在其中放邮件时,您每天都必须检查您的邮箱。如果您可以指示邮递员敲门,则无需进行此轮询工作,但这通常是不可能的。

链接事件逻辑

在许多编程语言中,您可以编写一个事件,该事件在键盘上的某个按键被按下或在特定时间被调用。尽管这些是外部事件,但您无需轮询它们。为什么?

这是因为操作系统正在为您轮询。例如,Windows检查诸如键盘状态更改之类的内容,如果检测到此错误,它将呼叫事件订阅者。因此,当您订阅键盘按下事件时,实际上是在订阅一个本身就是轮询事件的订阅者的事件。

打个比方,假设您住的是公寓楼,而一名邮政工作人员将邮件投送到公共邮件接收区。然后,类似操作系统的工作人员可以为每个人检查该邮件,然后将邮件传递给收到邮件的人的公寓。这免除了其他人不得不轮询邮件接收区域的麻烦。


我的直觉是假定事件侦听器不断检查事件是否已被触发,这意味着,在我的情况下,与检查事件是否被触发的每个帧没有什么不同。

根据课堂讨论,事件监听器似乎以不同的方式工作。

事件监听器如何工作?

如您所怀疑,事件可以通过轮询进行。而且,如果某个事件与外部事件有某种关系,例如按下键盘键,则轮询确实必须在某个时间点进行。

事实也不一定需要涉及轮询。例如,如果事件是按下按钮时,则该按钮的事件侦听器是GUI框架在确定鼠标单击击中按钮时可以调用的方法。在这种情况下,仍然必须进行轮询才能检测到鼠标单击,但是鼠标侦听器是通过事件链连接到原始轮询机制的更被动的元素。

更新:在低级硬件轮询上

事实证明,USB设备和其他现代通信协议具有一组非常引人入胜的类似于网络的交互协议,使包括键盘和鼠标在内的I / O设备可以参与特定的拓扑。

有趣的是,“ 中断 ”是必不可少的,同步的事物,因此它们无法处理临时网络拓扑。为了解决这个问题,“ 中断 ”已被普遍化为异步的高优先级数据包,称为中断事务(在USB上下文中)或消息信号中断(在PCI上下文中)。USB规范中描述了该协议:

在此处输入图片说明

- “ 图8-31散装/控制/中断OUT交易主机状态机 ”,在‘通用串行总线规范,修订版2.0’,印刷页-222; PDF-page-250(2000-04-27)

要点似乎是I / O设备和通信组件(如USB集线器)基本上起着网络设备的作用。因此,它们发送消息,这需要轮询其端口等。这减轻了对专用硬件线的需求。

像Windows这样的操作系统似乎可以自行处理轮询过程,例如MSDN文档中的USB_ENDPOINT_DESCRIPTOR其中描述了如何控制Windows多久轮询一次USB主机控制器以获取中断/同步消息:

bInterval值包含中断和同步端点的轮询间隔。对于其他类型的端点,应忽略此值。此值反映固件中设备的配置。驾驶员无法更改。

轮询间隔,以及设备的速度和主机控制器的类型,决定了驱动程序启动中断或同步传输的频率。中的值bInterval不代表固定的时间量。它是一个相对值,实际的轮询频率还取决于设备和USB主机控制器是以低速,全速还是高速运行。

- “ USB_ENDPOINT_DESCRIPTOR结构”,Microsoft硬件开发中心

较新的显示器连接协议(例如DisplayPort)似乎可以做到这一点:

多流传输(MST)

  • DisplayPort版本1.2中添加了MST(多流传输)

    • 在版本1.1a中仅提供SST(单流传输)
  • MST通过单个连接器传输多个A / V流

    • 多达63个流;不是“每车道流”

      • 这些传输流之间没有假定同步性;一个流可能处于消隐期,而其他流则不在
    • 面向连接的传输

      • 在流传输开始之前,通过AUX CH通过消息事务从流源到目标流宿的路径

      • 在不影响其余流的情况下添加/删除流

在此处输入图片说明

- “ DisplayPortTM Ver.1.2 概述”中的 14张幻灯片(2010-12-06)

这种抽象允许一些巧妙的功能,例如从一个连接运行3个监视器:

DisplayPort多流传输还允许将三个或更多设备连接在一起,但是采用相反的,较少面向“消费者”的配置:同时从单个输出端口驱动多个显示器。

- “ DisplayPort”,维基百科

从概念上讲,可以避免的是轮询机制允许更通用的串行通信,当您需要更通用的功能时,这真是棒极了。因此,硬件和OS会对逻辑系统进行大量轮询。这样,订阅事件的消费者就可以享受下级系统为他们处理的那些细节,而不必编写自己的轮询/消息传递协议。

最终,诸如按键之类的事件似乎经历了一系列相当有趣的事件,然后才进入软件级的命令式事件触发机制。


关于最后一段,通常不会在底层进行轮询,操作系统会对外围设备触发的硬件中断做出反应。计算机通常具有许多已连接的设备(鼠标,键盘,磁盘驱动器,网卡),而对所有设备进行轮询将非常低效。
Barmar

但是,您对邮件传递的类比正是我将解释更高级别活动的方式。
Barmar

1
@Barmar你知道,当设备移动到USB连接,有一个关于他们直接产生中断怎么去(像一个PS / 2键盘一样)到需要轮询(如USB键盘一样)谈了很多,并且一些消息来源要求轮询是由CPU完成的。但是,其他消息来源声称这是在专用控制器上完成的,该控制器将轮询转换为CPU的中断。
纳特

@Barmar您是否会知道哪个是正确的?我可能已经看到更多的消息来源声称CPU会执行轮询,但是使用专用的控制器似乎更有意义。我的意思是,我认为Arduino和其他嵌入式设备往往需要CPU来执行轮询,但是我对x86型设备一无所知。
纳特

1
如果有人可以确认以便我可以更新此答案,我认为现代I / O设备(例如通过USB连接的设备)可以直接写入内存,而无需CPU的控制(这就是为什么它们快速/高效和安全的原因有时会有危险)。然后,需要使用现代操作系统来轮询内存以检查是否有新消息。
纳特

8

拉与推

有两种主要策略可以检查事件是否发生或是否达到特定状态。例如,假设等待一个重要的交付:

  • :每隔10分钟,进入您的邮箱并检查是否已发送,
  • 推送:告诉送货员在送货时给您打电话。

的方法(也称为轮询)比较简单:你可以实现它没有任何特殊的功能。另一方面,它通常效率较低,因为您冒着进行额外检查而又无所作为的风险。

另一方面,推入方法通常更有效:您的代码仅在有事情要做时才运行。另一方面,它要求您存在一种机制来注册侦听器/观察者/回调1

1 不幸的是,我的邮递员通常缺乏这种机制。


1

具体来说,是关于统一-除了每帧轮询一次之外,没有其他方法可以检查玩家的输入。要创建事件侦听器,您仍然需要“事件系统”或“事件管理器”之类的对象来进行轮询,因此只会将问题推到另一个类。

当然,一旦有了事件管理器,就只有一个类在每一帧轮询输入,但这并没有明显的性能优势,因为现在该类必须遍历侦听器并对其进行调用,具体取决于您的游戏设计(例如,有多少听众以及播放器使用输入的频率)实际上可能会更昂贵。

除此之外,请记住黄金法则- 过早的优化是万恶之源,这在视频游戏中尤其如此,在电子游戏中,渲染每一帧的过程通常要付出如此高昂的代价,以至于像这样的小型脚本优化完全是微不足道的


我不会将中央事件循环视为优化,而是将其编写为更具可读性,可理解性的代码,而不是轮询遍及整个代码库。它还允许“合成”事件和不是来自轮询游戏引擎的事件。
BlackJack

@BlackJack我同意,我通常自己用这种方式编写代码,但是OP询问性能。顺便说一句,Unity出人意料地有许多类似这样的可疑代码设计决策,就像几乎到处都具有静态功能一样。
Dunno

1

除非您在OS / Framework中有处理诸如按钮按下或计时器溢出或消息到达之类的事件的支持,否则您将无论如何都必须使用轮询(在其下方)来实现此事件侦听器模式。

但是,不要仅仅因为您没有立即获得性能优势就放弃这种设计模式。这是无论是否有事件处理基础支持都应使用它的原因。

  1. 该代码看起来更清晰,更孤立(当然,如果正确实现)
  2. 基于事件处理程序的代码可以更好地承受更改(因为您通常只修改某些事件处理程序)
  3. 如果碰巧使用具有底层事件支持的平台-您可以重用现有的事件处理程序,而无需使用轮询代码。

结论-您很幸运地参加了讨论,并了解了轮询的一种替代方法。寻找机会在实践中应用此概念,您将欣赏代码的精美程度。


1

大多数事件循环都构建在操作系统提供的某些轮询多路复用原语之上。在Linux上,该原语通常是poll(2)系统调用(但可能是旧的select)。在GUI应用程序中,显示服务器(例如XorgWayland)正在与您的应用程序通信(通过socket(7)pipe(7))。还请阅读有关X Window系统协议和体系结构的信息

这样的轮询原语是有效的。实际上,当完成一些输入(并处理了一些中断)时,内核会唤醒您的进程。

具体而言,您的窗口小部件工具箱库与显示服务器通信,等待消息,并将这些消息分发到窗口小部件。QtGTK之类的工具包库非常复杂(数百行源代码)。您的键盘和鼠标仅由显示服务器进程处理(该进程将这些输入转换为发送到客户端应用程序的事件消息)。

(我正在简化;实际上情况要复杂得多)


1

在基于纯轮询的系统中,可能想知道何时发生某些特定操作的子系统将需要在该操作可能发生的任何时间运行一些代码。如果有许多子系统需要在某个不必要的唯一事件发生后的10ms内做出反应,则它们都必须至少每秒检查100次,是否已经发生了它们的事件。如果这些子系统位于不同的线程(或更糟的是进程)进程中,则需要在每个此类线程或进程中切换100x /秒。

如果应用程序要监视的许多事情非常相似,那么拥有一个集中的监视子系统(也许是表驱动)可能会更有效率,该子系统可以监视许多事物并观察它们是否已更改。例如,如果有32个开关,则平台可能具有一次将所有32个开关读为一个字的功能,从而使监视代码可以检查在轮询之间是否更改了任何开关,如果没有,则不担心哪些代码可能会对它们感兴趣。

如果有许多子系统希望在某些情况发生变化时进行通知,那么拥有专用的监视子系统可以在事件发生时通知其他子系统,这些子系统可能会对它们感兴趣,这可能比让每个子系统轮询自己的事件更有效。但是,如果没有人对任何事件感兴趣,则建立一个专用的监视子系统将纯粹浪费资源。如果只有几个子系统对事件感兴趣,那么让他们监视他们感兴趣的事件的成本可能会比建立通用专用监视子系统的成本要低,但收支平衡不同平台之间的差异会很大。


0

事件侦听器就像耳朵在等待消息。事件发生时,被选择为事件侦听器的子例程将使用事件参数进行工作。

总有两个重要数据:事件发生的时刻和事件发生的对象。其他争论是关于发生了什么的更多数据。

事件侦听器指定对发生的反应。


0

事件监听器遵循发布/订阅模式(作为订阅者)

最简单的形式是,发布对象维护需要发布内容时要执行的订户指令列表。

它将具有某种subscribe(x)方法,其中x取决于事件处理程序设计为处理事件的方式。调用subscribe(x)时,x被添加到订户指令/参考的发布者列表中。

发布者可能包含用于处理事件的全部,部分或全部逻辑。事件发生时,它可能仅需要引用订户以使用其指定的逻辑来通知/转换它们。它可能不包含逻辑,并且需要可以处理事件的订阅者对象(方法/事件侦听器)。它最有可能同时包含两者。

当事件发生时,发布者将遍历并执行其订户指令/参考列表中每个项目的逻辑。

无论事件处理程序看起来多么复杂,它的核心都遵循这种简单的模式。

例子

对于事件侦听器示例,您向事件处理程序的subscribe()方法提供了一个方法/函数/指令/事件侦听器。事件处理程序将方法添加到其用户回调列表中。发生事件时,事件处理程序将遍历其列表并执行每个回调。

举一个真实的例子,当您在Stack Exchange上订阅新闻通讯时,对您的个人资料的引用将被添加到订阅者的数据库表中。当该发布新闻时,该参考将用于填充该新闻的模板,并将其发送到您的电子邮件中。在这种情况下,x只是对您的引用,发布者具有一组用于所有订阅者的内部指令。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.