iOS中“事件”的前因后果

article/2025/11/7 23:08:32

iOS的事件是一个由触发行为到响应的过程。本文旨在表达事件如何处理响应,如何传递事件的。

1. 前言

     国内智能手机要从2000年开始说起,手机的进化是飞速的,单从操作来看,最开始是数字实体键盘,后来出现了全字母的实体键盘。2007年首台iPhone诞生让我们看到了新的操作方式,它只有一个按键。2008年诺基亚5800作为第一款全电阻屏膜触摸手机,同年魅族M8全电容屏触摸手机紧随其后。至此手机操作已完全脱离实体键盘并且可以多点触控。言归正传本文不去探索电容屏的多点触控功能和原理,只剖析一下iPhone是如何通过人的一根手指触摸到最终响应的过程。本文以触摸事件为例。

iOS开发中常见的事件有以下4种(※不包含3Dtouch):

2. 事件相关类

UITouch:

定义:保存着跟手指相关的信息,比如触摸的位置、时间、阶段、大小、运动、力、角度等,一根手指只对应一个UITouch对象。

生命周期:当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置,当手指离开屏幕时,系统会销毁相应的UITouch对象。

UIEvent:

定义:事件对象,记录事件产生的时刻和类型,每产生一个事件,就会产生一个UIEvent对象。

UIResponder:

定义:响应者对象,用来响应用户的操作,处理各种事件。

封装了UIEvent、UITouch、UIPress、UIMenuBuilder等事件相关类,目的为其子类提供一系列方法可以重写处理,并可以用来获取响应的状态及相关属性。四个常用的touch方法就在这个类里定义的:

// 一根或多根手指开始触摸屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或多根手指在屏幕上移动,注意此方法在移动过程中会重复调用
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或多根手指触摸结束离开屏幕
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 触摸意外取消(例如正在触摸时打入电话)
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

UIGestureRecognizer:

它实现了上面的四个touche方法,但是它不是一个UIResponder子类,因此它并不在响应链中。

UITouch 和 UIEvent 提供了一些方法来获取触摸和手势识别的关联性。UITouch的gestureRecognizers 会列举出当前处理这个touch的所有手势。

touchesForGestureRecognizer: 方法列举了由指定手势识别所处理的触摸。

3. 两个函数

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

这2个函数定义在UIView中,因此只有UIView及其子类才可以重写这2个方法,通过遍历调用视图栈中每个视图的这两个函数来查找第一响应者。接下来分别来说一下他们的作用。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

该方法会被系统调用(可重写),其主要作用是查找命中视图:在视图的层次结构中寻找到一个最适合的视图 (理论为最上层视图)来响应触摸事件,如果返回为nil,即事件有可能被丢弃。此过程也就是查找第一响应者的过程。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:方法内部会通过调用这个方法,来判断点击区域是否在当前视图上,是则返回YES,不是则返回NO。当然我们也可以重写此方法来扩展点击区域,事件区分处理等操作。

4. 事件的传递过程

响应者链条:是由多个响应者对象(UIResponder对象)连接起来的链条,它让每个响应者之间存在链式的联系,并且可以让一个事件由多个对象处理

下面我们先看一下事件的传递流程,主要分成2大部分:

1.UIApplication接受事件前:

1.系统通过IOKit.framework来处理硬件操作,其中屏幕处理也通过IOKit完成(IOKit可能是注册监听了屏幕输出的端口)当用户操作屏幕,IOKit收到屏幕操作,会将这次操作封装为IOHIDEvent对象。通过 mach_Port (IPC进程间通信)将事件转发给SpringBoard来处理。

2.SpringBoard收到mach_Port发过来的事件后,会唤醒main runloop来处理。main runloop将事件交给source1处理,source1会调用__IOHIDEventSystemClientQueueCallback()函数。

3.函数内部会判断,是否有程序在前台显示,如果有则通过mach_PortIOHIDEvent事件转发给这个程序。如果前台没有程序在显示,则表明SpringBoard的桌面程序在前台显示,也就是用户在桌面进行了操作。__IOHIDEventSystemClientQueueCallback()函数会将事件交给source0处理,source0会调用__UIApplicationHandleEventQueue()函数,函数内部会做具体的处理操作。

4.假如当前有程序处于活跃状态,在__UIApplicationHandleEventQueue()函数中,会将传递过来的IOHIDEvent对象转换为UIEvent对象

5.在函数内部,调用UIApplicationsendEvent:方法,将UIEvent对象传递给第一响应者或UIControl对象处理。


总结一下:通过mach_Port将触摸事件转发给SpringBoard来处理,它会唤醒main runloop并触发source1的回调,接着会通过函数调用source0回调,source0的回调会将事件放入当期活动的UIApplication事件队列里,并通过sendEvent:方法直接调传递给第一响应者。(这里有2个特殊存在,UIControl和UIGestureRecognizer本身及其子类会优先响应接受事件,他们会跳过事件相应链的查找)。

备注:

SpringBoard:是iOS中的桌面管理器,它是iOS程序中,事件的第一个接受者。它只能接受少数的事件比如:按键(锁屏/静音等),触摸,加速,接近传感器等几种事件,随后使用mach_Port转发给需要的App进程。

Source1:基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好。

Source0:非基于Port的处理事件,简单来说就是这个消息是负责App内部事件,由App负责管理触发的。

2.UIApplication接受到事件后:

1.UIApplication接收到事件,将事件传递给keyWindow。(注:keyWindow一般为多个最后添加的window)

2.keyWindow遍历subViewshitTest:withEvent:方法,找到点击区域内合适的视图来处理事件。

3.UIView的子视图也会遍历其subViewshitTest:withEvent:方法,以此类推。

4.直到找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication。

5.在查找第一响应者的过程中,已经形成了一个响应者链。

6.应用程序会先调用第一响应者处理事件。

7.如果第一响应者不能处理事件,则 调用其nextResponder方法,一直找响应者链中能处理该事件的对象。

8.最后到UIApplication后仍然没有能处理该事件的对象,则该事件被废弃。

*在事件传递过程中hitTest:withEvent:方法至关重要,它的查找过程大致如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{    // *---------------- hitTest实现过程 ----------------*// 1.判断当前控件能否接收事件if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {return nil;}// 2. 判断点在不在当前视图上if ([self pointInside:point withEvent:event] == NO) {return nil;}// 3.从后往前遍历自己的子控件NSInteger count = self.subviews.count;for ( int i = 0; i < count; i++){UIView *subView = self.subviews[count - 1 - i];//进行坐标转化CGPoint coverPoint = [subView convertPoint:point fromView:self];// 调用子视图的 hitTest 重复上面的步骤。找到了,返回hitTest view ,没找到返回有自身处理UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];if (hitTestView){return hitTestView;}}// 循环结束,表示没有比自己更合适的viewreturn self;
}

看代码我们会发现有4种情况:会直接返回nil,也就是不接受触摸事件,这4种情况分别为:

1. 视图的hidden等于YES。

2. 视图的alpha小于等于0.01

3. 视图的userInteractionEnabledNO

4. 视图超出父视图的bounds

注意这里不包括clearColor

当我点击B视图的时打印如下:

>:-[RootWindow hitTest:withEvent:]<-
>:-[RootView hitTest:withEvent:]<-
>:-[CView hitTest:withEvent:]<-
>:-[AView hitTest:withEvent:]<-
>:-[RootWindow hitTest:withEvent:]<-
>:-[RootView hitTest:withEvent:]<-
>:-[CView hitTest:withEvent:]<-
>:-[AView hitTest:withEvent:]<-

在这里A、C视图是同级子视图,并且A先添加,C后添加。可以分析得出若有多个同级视图时,会按添加顺序添加的逆序去查找。实际上不难看出,这个处理流程有点类似二分查找的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。

5. UIControl

UIControl是继承于UIView的,我们实际测试中它也确实是通过hitTest:withEvent:的方式查找第一响应者的,唯一区别就是当UIControl及其子类为第一响应者时会直接由UIApplication派发事件,无需通过事件响应连逐级传递。我猜测这也是苹果为什么要封装一个UIControl这个类的原因之一。以UIButton为例:

 

6. UIGestureRecognizer

UIGestureRecognizer是手势识别器,它直接继承于NSObject的。当响应者链和手势同时出现时,也就是既实现了touches方法又添加了手势,会发现touches方法有时会失效,这是因为手势的执行优先级是高于响应者链的。

根据苹果的官方文档,手势不参与响应者链传递事件,但是也通过hitTest的方式查找响应的视图,手势和响应者链一样都需要通过hitTest:withEvent:方法来确定响应者链的。在UIApplication向响应者链派发消息时,只要响应者链中存在能够处理事件的手势,则手势响应事件,如果手势不在响应者链中则不能处理事件。

详见苹果文档:UIGestureRecognizer

7. 遇到的问题

1. 在测试中发现,每一次触摸操作,会触发两次hitTest:withEvent:方法。 两次调用的point参数完全相同,时间戳也相同。 但是两次的调用栈不同。

最终,在 cocoa-dev 邮件列表中找到了对应话题的讨论, 大致是这样说的,系统可能会在两次调用中,做一些点击位置的微调。但是在目前的情况下,并没有发现点击位置 point 的变化。 所以,在此处我对于重复调用的情况进行了过滤,以避免执行重复的点击逻辑。

可以参考苹果官方博客: -hitTest:withEvent: called twice?

2. 在调用touchesBegan:withEvent:时,用super和nextResponder结果是一样的,都不会导致响应链错乱。

这是由于super关键字并非“父类”那么简单,它本质是一个编译器标示符

可以参考文章:iOS的“super”关键字

3. 同一个按钮既添加了手势,又添加了target-action事件,那么会优先响应哪个?

上文已经提到了,手势的优先级高于UIResponder的,因此会优先执行。假设要想让2个都执行可行吗?可行只需要设置UIGestureRecognizer的属性cancelsTouchesInView=NO即可解决。

4. 在iOS13系统里,当下一响应者找到keyWindow时,在去打印nextResponder,会打印如下:

这是由于iOS13的新特性:UIWindowScene,支持多场景窗口(多任务)。

8. 实战

学了这么多究竟能用它做什么呢?我给出几个实例,只是抛砖引玉而已:

1. 通过视图查找视图所在的控制器

- (UIViewController *)parentController {UIResponder *responder = [self nextResponder];while (responder) {if ([responder isKindOfClass:[UIViewController class]]) {return (UIViewController *)responder;}responder = [responder nextResponder];}return nil;
}

2. 通过hitTest:withEvent:方法,实现超出父视图边界时依然可以让当前视图响应点击。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {//将当前A视图中的触摸点转换坐标系,转换到B视图的身上,生成一个新的点CGPoint newPoint = [self convertPoint:point toView:self.bView];//判断如果这个点是在B视图上,那么处理点击事件最合适的视图返回B视图if ( [self.bView pointInside:newPoint withEvent:event]){return self.bView;}return [super hitTest:point withEvent:event];}

3. 通过hitTest:withEvent:方法,实现扩大按钮的点击范围,四边都扩大10个像素点。

// 重新命中视图扩大手势范围
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {CGRect largeRect = CGRectMake(self.button.bounds.origin.x - 10, self.button.bounds.origin.y - 10, self.button.frame.size.width + 10 * 2, self.button.frame.size.height + 10 * 2);if (CGRectContainsPoint(largeRect, point)) {return self.button;}return [super hitTest:point withEvent:event];}

当然也可以使用pointInside:withEvent:来实现

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {CGRect largeRect = CGRectMake(self.button.bounds.origin.x - 10, self.button.bounds.origin.y - 10, self.button.frame.size.width + 10 * 2, self.button.frame.size.height + 10 * 2);if (CGRectEqualToRect(largeRect, self.bounds)) {return [super pointInside:point withEvent:event];}else {return CGRectContainsPoint(largeRect, point);}
}

至此本文结束,有任何问题可随时联系,欢迎共同探讨研究(*^▽^*)。


http://chatgpt.dhexx.cn/article/Oku2mo94.shtml

相关文章

AppCrash explorer问题(解决方法)

AppCrash 顾名思义 程序崩溃了 先上问题&#xff1a; 资源管理器不断地显示停止工作&#xff08;关闭后10s又出来报错&#xff09; 我也在网上找了很多资料&#xff0c;还去Google搜了一下&#xff0c;但我的错误和dll这个无关&#xff0c;修改注册表等方法都无法解决此问题&a…

win7 APPCRASH问题解决!

真是废了老劲了。。什么清理插件&#xff0c;各种运行msconfig/启动都试了 问题&#xff1a;**.exe已停止工作 问题事件名称: APPCRASH 应用程序名: compute_image_mean.exe 应用程序版本: 0.0.0.0 应用程序时间戳: 579c50f5 故障模块名称: MSVCR120.dll 故障模块版本: 12.0.21…

mysql安装appcrash_appcrash事件怎么解决-appcrash问题解决方法 - 系统家园

在电脑上运行程序的时候常常会遇到很多的问题然后提示appcrash错误&#xff0c;为此下面就给你们带来了appcrash问题解决方法&#xff0c;一直遇到这个问题的小伙伴就快来解决一下吧。 appcrash事件怎么解决&#xff1a; 方法一&#xff1a; 1、出现appcrash错误会提示给你故障…

程序崩溃APPcrash的问题

【问题】 小编在对接扫码枪枪的时候遇到了一个问题&#xff1a;拔出扫码枪的时候&#xff0c;有状态返回但是出现了这个错误&#xff1a; 【解决办法】 小编我真是心累的“狠”&#xff0c;但经过me的不辞辛苦还是找到了问题的根本&#xff1a; 一、厂家提供的环境 二、小编…

问题事件名称: APPCRASH(解决方法)

问题事件名称: APPCRASH(解决方法)(转&#xff09; 下面分享一下解决win7或者是Vista的一个刺手的问题 APPCRASH&#xff08;app是程序的意思&#xff0c;crash是坠机的意思。就是程序崩溃了/程序撞车……&#xff09; 我们使用软件的时候有时候会出现这种情况 举个例子 Dung…

appcrash事件怎么解决?三种方法教你

我们在电脑上运行程序的时候常常会遇到很多的问题然后提示appcrash错误&#xff0c;为此下面小编就给你们带来了appcrash问题解决方法&#xff0c;有遇到这个问题的小伙伴就快来解决一下吧。 appcrash事件怎么解决&#xff1f; 方法一 1、出现appcrash错误会提示给你故障模块&a…

LoadRunner9.1下载与破解

LoadRunner9.1下载与破解 上一篇 / 下一篇 2009-03-03 18:42:39 / 个人分类&#xff1a;测试工具 查看( 918 ) / 评论( 2 ) / 评分( 0 / 0 ) 一、Loadrunner下载地址&#xff1a; http://h30302.www3.hp.com/prdownloads/T7177-15005.iso?ordernumber380397475&itemid1&…

Loadrunner11破解详解 .

使用说明&#xff1a;要以管理员的身份运行 1、正常安装完LR11后。然后双击deletelicense.exe 2、然后解压替换其中的2个DLL文件拷贝到"C:\Program Files\HP\LoadRunner\bin\"下替换原有文件 3、进入LR&#xff0c;输入以上的序列号即可 global-100: AEACFSJI-YASEKJ…

LoadRunner11的安装与破解

现在很多人都在用LoadRunner11&#xff0c;下面我就来说说自己下载和安装LR11的过程。 一、安装 1.先来下载LR11.建议大家都在正规网站下载&#xff0c;推荐一个网站http://bbs.51testing.com/thread-423695-1-1.html&#xff0c;直接将此网站复制到迅雷下载http://www.genil…

Loadrunner11在win10下的安装、汉化与破解方法

Loadrunner11安装与破解方法 工具/原料 • HP Loadrunner 11.00 • 汉化包 • 破解文件 1 安装英文版 1.1 运行“setup.exe” 点击安装&#xff0c;其中会有提示缺少“Microsoft Visual C 2005 SP1运行组件”&#xff0c; 在“\lrunner\Chs\prerequisites\vc2005_sp1_redis…

loadrunner11基础使用

其实loadrunner11只要环境装好了&#xff0c;没那么多报错 装好Loadrunner后要用管理员权限打开&#xff0c;不然可能会报错 win10可以装lr12和12.5&#xff0c;但是无法破解&#xff0c;最大并发50人&#xff0c;还只有7天试用期&#xff0c;7天过了要重新装 loadrunner11中主…

loadrunner 11下载及破解

原文地址为&#xff1a; loadrunner 11下载及破解 1.下载参照文章&#xff1a; http://www.51testing.com/?uid-4827-action-viewspace-itemid-225451 2.破解参照文章&#xff1a; http://naotang.com/index.php?optioncom_content&viewarticle&id66:loadrunner11&a…

LoadRunner11的安装、破解以及基本使用

首先安装环境 除此之外没啥了&#xff0c;安装时会提示缺少什么组件&#xff0c;点击确认安装即可&#xff0c;剩下基本都是下一步就OK。 安装完成后自动启动会提示只剩余10天试用期 之后开始破解 选择“新许可证”,输入“ AEAMAUIK-YAFEKEKJJKEEA-BCJGI” 再次打开程序&…

LoadRunner测试工具大全下载,破解,licence

LoadRunner测试工具大全下载&#xff0c;破解&#xff0c;licence。 目前的版本有7.8&#xff0c;8.0&#xff0c;8.1下面是他们的下载地址&#xff0c;大家看自己的情况下载 安装程序资源 &#xff08;eMule资源&#xff09; 《工业标准级负载测试工具 LoadRunner》(LoadRunne…

loadrunner 11 的下载和安装与破解

Loadrunner安装详解 一 、 下载篇 下载地址是&#xff0c;这个是我自己搜集的&#xff0c;也可以安装其他版本&#xff1a; 链接&#xff1a;http://pan.baidu.com/s/1skGkzb3 密码&#xff1a;88uq loadrunner 12 链接: https://pan.baidu.com/s/1qXL4tFu 密码: cgrt …

loadrunner11.0 安装 破解

一 、下载(4G多):ed2k://|file|%5B%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95%E5%B7%A5%E5%85%B7LR11.00%5D.loadrunner-11.iso|4313382912|b95afeb3a879c6b8146435bb79e8cb97|hntupkaxx5ltzwovgdkv7uexj7ukal5hu|/ 二 、安装: 1.首先我安装了一个加载ISO光盘的虚拟光驱的工具: …

loaderrunner11.00安装与破解

在下面的链接里面下载安装包&#xff0c;语言包&#xff0c;以及破解程序 http://pan.baidu.com/s/1bnEQmEr 安装完安装包&#xff0c;语言包后&#xff0c;关键看怎么破解&#xff0c;步骤如下&#xff1a; 1. lm70.dll文件&#xff0c;覆盖\LoadRunner\bin下文件即可。 mlr5l…

Loadrunner安装破解

右键用软碟通加载 选择loadrunner安装镜像>打开 双击打开 setup >以管理员身份运行 完整安装&#xff0c;记得千万不要汉化 否&#xff0c;继续安装 默认&#xff0c;确定 默认&#xff0c;下一步 同意协议&#xff0c;下一步 用户信息&#xff0c;默认…

loadrunner 11 的下载和安装

Loadrunner安装详解 一 、 下载篇 下载地址是&#xff0c;这个是我自己搜集的&#xff0c;也可以安装其他版本&#xff1a; 链接&#xff1a;http://pan.baidu.com/s/1skGkzb3 密码&#xff1a;88uq 二、 安装篇 如果已有安装包&#xff0c;可从这一步直接开始&#xff0c;建…

win10下loadrunner11安装与破解

一&#xff0e;安装 1. 下载loadrunner11进行安装。以管理员身份打开安装安装程序进入如图界面 &#xff08;可能会出现提示&#xff0c;不受信任。出现这个情况尝试用此方法解决下“winr”打开运行&#xff0c;输入gpedit.msc进入组策略&#xff0c;依次选择“计算机配置…