Mac开发跬步积累(六): 响应链NSResponder Chain

濒危物种-耳廓狐

关于macOS 事件响应架构 可以参看我的另一篇文章macOS AppKit 的事件响应简介,本文是对事件响应的经一步实践与讨论,通过代码细节来展示一些实际开发中的问题与原因,仅供学习讨论.

0x00 什么是响应链

响应链是一种消息处理机制,它是由一组有序响应者对象组成的链条.当消息进入响应链条后,由响应者对象依次判断是否能够处理该消息,当一个响应者对象不能处理此条消息时,它会将消息传递给它的继任者(也就是它的下一个响应者对象). 响应链具有如下特性:

  • App Kit自动创建的;
  • 一个App可以包含任意数量的响应链,但同一时刻仅能有一条响应链处理消息;
  • 可以在响应链中插入响应者:(通过NSRespondersetNextResponder:方法);
  • 不同的事件消息,在响应链中会有不同的响应逻辑;

0x01 响应消息的种类

响应链处理的消息大体上分为两种:Event MessagesAction 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, main window 与 key window 相同

非文档App 无NSWindowController,且主Windowkey Window不同 的响应链图例:
非文档App-无NSWindowController, key window 与 main window 不同

非文档App 有NSWindowController响应链图例:
非文档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开始,由于ViewControllerView没有实现mouseDown:响应事件,所以响应链会将事件接着传递给View的下一个响应者(就是ViewController),因此我们可以看到正常信息输出;

ViewController响应mouseDown:

为了验证响应链的事件传递过程,我们在工程中添加自定义XCResponseView,并实现mouseDown:事件处理逻辑,运行代码从控制台中的信息可以看出,鼠标事件是XCResponseView类输出,而ViewController没有输出(尽管ViewController也实现了mouseDown:方法)

XCResponseView mouseDown:

因此我们得到的 mouseDown:事件的响应链图如下:

XCResponseView Responder Chain

在理解鼠标事件的响应顺序后,那么问题来了,为什么键盘事件没有响应呢?显然ViewController中我们已经实现了keyDown:方法;在回答这个问题之前,我们先看一下网络上普遍关于NSViewController监听键盘事件的方法:使用NSEvent添加本地事件监听

NSEvent addLocalMonitor

代码运行后,可以实现键盘事件的处理,但为了更细致的了解响应链过程,我们并不使用这个方案,那么我们再来回顾一下"Event Message"中对于键盘事件的描述:

键盘事件响应开始

键盘事件鼠标事件起始响应者是不一样的,viewDidAppear方法中,我们添加代码查看一下:当前窗口的第一响应者对象信息:

窗口的第一响应者

根据控制台信息,我们可以看出键盘事件的第一响应者是当前窗口对象NSWindow,在键盘事件整个响应链中,ViewController是被忽略的,所以ViewController中的keyDown:方法没有机会被执行;

window first responder

由此可知,如果需要ViewController响应键盘事件,我们需要告知NSWindow对象,它的下一个响应者ViewController即可.代码如下:

设置响应者

变更后的响应链如图:

修改后的响应链效果

代码运行后,点击键盘(功能键除外)可以看到ViewControllerkeyDown:方法正常输出:

Controller 的keyDown:

尽管使用上面的方法,我们完成了ViewController键盘事件的响应,但是却改变了原来的响应链结构,姿势不够优雅,那么有没有不改变响应链结构,仍然可以让ViewController响应键盘事件的方法呢? 答案:是改变第一响应者,因为键盘事件是从第一响应者开始的! 我们需要将响应链设置为下图的效果即可:(View获取键盘事件后如果自己不响应,就会依据响应链传递给ViewController)

修改第一响应者

根据前文0x03 第一响应者 内容可知,我们只需要让自定义的XCResponseView实现acceptsFirstResponder方法并返回YES即可:

开启第一响应者

运行代码,查看控制台信息,第一响应者是XCResponseView,而且ViewController响应了键盘事件!

控制台信息

0x05 一些思考

本文通过示例抛砖引玉,仅仅讨论学习响应链的冰山一角,希望对学习macOS事件响应机制有所帮助,为了大家能够更深入了解响应链,留一些思考问题,激发大家的主动学习姿势:

  • NSEventaddLocalMonitorForEventsMatchingMask: handler:方法中,handler中为什么返回值?

  • 控制器(NSViewController)中运行代码[self.view setNextResponder:nil];的效果与期望一样么?

  • NSWindowmakeFirstResponder: 生效的条件是什么?

  • NSViewController实现acceptsFirstResponder方法并返回YES 有效果么? 为什么?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 在AppKit中的事件都处于一个响应的链条中,这个链条是由一个叫做NSResponder 的类定义的,这个响应链条...
    代码行者阅读 6,282评论 3 8
  • 本文转自(原文太杂乱,这里调整了格式及内容):http://enkichen.com/2018/09/12/osx...
    topws1阅读 18,261评论 0 27
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,470评论 1 11
  • 一. Hit-Testing 什么是Hit-Testing?对于触摸事件, window首先会尝试将事件交给事件触...
    面糊阅读 814评论 0 50