关于macOS 事件响应架构 可以参看我的另一篇文章macOS AppKit 的事件响应简介,本文是对事件响应的经一步实践与讨论,通过代码细节来展示一些实际开发中的问题与原因,仅供学习讨论.
0x00 什么是响应链
响应链
是一种消息处理机制,它是由一组有序
的响应者对象
组成的链条.当消息
进入响应链条后,由响应者对象
依次判断是否能够
处理该消息,当一个响应者对象
不能处理此条消息
时,它会将消息传递给它的继任者(也就是它的下一个响应者对象)
. 响应链具有如下特性
:
- 由
App Kit
自动创建的; - 一个App可以包含
任意数量
的响应链,但同一时刻
仅能有一条响应链
处理消息; - 可以在响应链中
插入
响应者:(通过NSResponder
的setNextResponder:
方法); -
不同的
事件消息,在响应链中会有不同的
响应逻辑;
0x01 响应消息的种类
响应链
处理的消息大体上分为两种
:Event Messages和Action Messages
Event Messages(事件消息):
Event Messages
主要指的是由键盘/鼠标/触控板
触发的NSEvent事件
.几乎所有的Event Messages
都由当前窗口对象(NSWindow)
的响应链进行处理;事件消息
的处理起始
于NSWindow的第一个派发对象
.
- 对于
键盘
事件, 响应是从窗口的第一响应者
开始; - 对于
鼠标/触控板
事件,响应是从用户操作的view
开始;
如果事件消息在最初没有响应
,那么响应链将按照视图的层级结构
依次传递消息,直到窗口对象(NSWindow)
为止,如果当前窗口对象(NSWindow)
是由NSWindowController
管理的,那么这个NSWindowController
将会成为最终
的事件响应者;当整个响应链都没有完成对事件的处理时,响应链会调用最后响应者
的noResponderFor:
方法,可以根据具体的需求来重写这个方法
实现相应的功能;
Action Messages(行为消息):
Action Messages
主要是指一些操作指令
的行为事件,比如"翻到下一页"
,"移动到文章的最后一行"
,或"移动到行首(行尾)"
等操作指令行为;App Kit
构建处理Action Messages
的响应链时,主要依据下面两种情况:
-
App
是否基于文档结构
(如果非文档结构App
,则判断window
是否有NSWindowController
管理); -
App
是否显示key window
以及main window
;
非文档App 无NSWindowController
,且主Window
即为key Window
的响应链
图例:
非文档App
无NSWindowController
,且主Window
与key Window
不同 的响应链
图例:非文档App 有
NSWindowController
的响应链
图例:0x02 响应者
响应者
是一个能够接收消息
的对象,并且可以响应行为
,响应者通常都继承自NSResponder
;例如App Kit中的NSApplication
, NSWindow
, NSDrawer
, NSWindowController
, NSView
等均是如此; 响应者是构成响应链
中的一部分.
0x03 第一响应者
第一响应者
是指用户通过鼠标
或者键盘
选择的交互对象;它通常是整个响应链
中的第一个
响应者对象,NSWindow对象
的最初始第一响应者是它自己
,当window
显示在屏幕
上时,也可以手动设定
它的第一响应者对象(使用NSWindow对象
的makeFirstResponder:
方法).
当一个NSWindow对象
在接收到鼠标点击(mouse-down)
事件时,会自动设置
鼠标所处的View
为第一响应者
;那么NSWindow对象
如何确认某个对象
是否能够
成为第一响应者呢?答案是调用对象
的acceptsFirstResponder方法
获取结果;这个方法默认返回NO
;如果某个响应者
对象希望成为第一响应者
,那么它需要重写
这个方法,并返回YES
;
需要注意的一个事件是:Mouse-moved,它总是发送给第一响应者
,而不是鼠标所在的视图View
;
0x04 从一个实际"栗子"开始
项目示例代码地址:ResponderChainDemo
理论结合实践
,让我们通过一个实际
项目示例来尝试
学习响应链
的事件处理
.
在
ViewController
中实现键盘按下
事件/鼠标点击
事件 并在视图加载完毕
后,输出响应链
信息:
代码运行结果:
鼠标事件
正常响应,但键盘事件
没有获得响应! 根据输出的响应链
信息,绘制响应链
如下图:
根据前文
Event Message
中讲到的鼠标/触控板
事件是从用户操作的View
开始,由于ViewController
的View
没有实现mouseDown:
响应事件,所以响应链
会将事件接着传递给View的下一个响应者
(就是ViewController
),因此我们可以看到正常信息输出;
为了
验证
响应链的事件传递过程
,我们在工程中添加自定义XCResponseView
,并实现mouseDown:
事件处理逻辑,运行代码从控制台中的信息
可以看出,鼠标事件是XCResponseView
类输出,而ViewController
没有输出(尽管ViewController
也实现了mouseDown:
方法)
因此我们得到的
mouseDown:
事件的响应链图
如下:
在理解
鼠标事件
的响应顺序后,那么问题来了,为什么键盘事件
没有响应呢?显然ViewController
中我们已经实现了keyDown:
方法;在回答这个问题之前,我们先看一下网络上普遍
关于NSViewController
监听键盘事件
的方法:使用NSEvent
添加本地事件监听
代码运行后,可以实现
键盘事件的处理
,但为了更细致的了解响应链
过程,我们并不使用这个方案
,那么我们再来回顾
一下"Event Message"
中对于键盘事件
的描述:
键盘事件
与鼠标事件
的起始响应者
是不一样的,在viewDidAppear
方法中,我们添加代码
查看一下:当前窗口的第一响应者对象
信息:
根据
控制台信息
,我们可以看出键盘事件
的第一响应者是当前窗口
对象NSWindow
,在键盘事件
的整个
响应链中,ViewController
是被忽略的,所以ViewController
中的keyDown:
方法没有机会被执行
;
由此可知,如果需要
ViewController
响应键盘事件
,我们需要告知NSWindow
对象,它的下一个响应者
是ViewController
即可.代码如下:
变更后的
响应链
如图:
代码运行后,
点击键盘(功能键除外)
可以看到ViewController
的keyDown:
方法正常输出:
尽管使用
上面的方法
,我们完成了ViewController
对键盘事件
的响应,但是却改变
了原来的响应链结构
,姿势不够优雅
,那么有没有不改变
响应链结构,仍然可以让ViewController
响应键盘事件
的方法呢? 答案:是改变第一响应者,因为键盘事件
是从第一响应者
开始的! 我们需要将响应链设置为下图的效果即可:(View获取键盘事件后如果自己不响应,就会依据响应链传递给ViewController)
根据前文0x03 第一响应者 内容可知,我们只需要让自定义的
XCResponseView
实现acceptsFirstResponder
方法并返回YES
即可:
运行代码,查看
控制台
信息,第一响应者是XCResponseView
,而且ViewController
响应了键盘
事件!
0x05 一些思考
本文通过示例
抛砖引玉,仅仅讨论学习响应链
的冰山一角,希望对学习macOS事件响应机制
有所帮助,为了大家能够更深入了解
响应链,留一些思考问题,激发大家的主动学习
姿势:
NSEvent
的addLocalMonitorForEventsMatchingMask: handler:
方法中,handler
中为什么返回值
?在
控制器(NSViewController)
中运行代码[self.view setNextResponder:nil];
的效果与期望一样么?NSWindow
的makeFirstResponder:
生效的条件是什么?NSViewController
实现acceptsFirstResponder
方法并返回YES
有效果么? 为什么?