iOS 事件传递和处理

前言

iPhone拥有很好的用户交互体验,这源于iOS系统对交互事件的高效处理和高优响应;
App开发者处理用户交互非常便捷,这源于iOS系统和UIKit对用户操作做了封装和默认处理;
本文围绕iOS的事件传递和处理,探究其具体过程。

正文

什么是事件?

这里讲的事件是用户交互的抽象,像IOHIDEvent和UIEvent都是不同处理阶段的封装。

IOHIDEvent是iOS系统对事件的封装,感兴趣可以看源码IOHIDEvent.hIOHIDEvent.cpp(HID是Human Interface Device的缩写)。

UIEvent是UIKit封装的描述用户操作类型的对象,可能有touch事件、motion事件、remote-control事件、press事件等。不同事件在响应链中处理方式不同,这里我们主要分析touch事件的传递和处理。

用户点击手机屏幕的过程

App外:用户点击->硬件响应->参数量化->数据转发->App接收。

在用户触摸屏幕之后,屏幕硬件会接受用户的操作,并采集关键的参数传递给IOKit,而IOKit将这些数据打包并传给SpringBoard.app,继而转发给前台App。

App内:子线程接收事件->主线程封装事件->UIWindow启动hitTest确定目标视图->UIApplication开始发送事件->touch事件开始回调。

App启动时便会启动一个com.apple.uikit.eventfetch-thread子线程,负责接收SpringBoard.app转发过来的数据(通过runloop监听source1,查看堆栈中有__CFRunLoopDoSource1),数据会被封装成IOHIDEvent对象,然后转发给主线程;

主线程同样在启动时监听source0,接收eventfetch-thread线程发送的IOHIDEvent数据,再封装成UIEvent,根据UIEvent的类型判断是否需要启动hitTest。motion事件不需要hitTest,touch事件也有部分不需要hitTest,比如说touch结束触发的事件。

确定目标视图之后,UIApplication便会发送事件,将UITouch和UIEvent发送给目标视图,触发其touches系列的方法。

UIKit寻找目标视图的过程

寻找的过程主要依赖两个UIView的方法:-hitTest:withEvent方法和-pointInsdie:withEvent方法。

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

hitTest方法返回point和event对应的视图;

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

pointInside方法返回point和event是否在自己当前视图上;

这两个方法UIView都提供了默认实现,hitTest方法默认会调用所有子视图的hitTest方法,如果有一个返回。

UIKit会从UIWindow开始寻找目标视图,先调用UIWindow的hitTest方法询问是否有响应的视图,hitTest方法首先会先调用UIWindow的pointInside方法询问是否在点击范围内。

a.如果pointInside方法返回NO,则证明UIWindow无法响应该事件,hitTest方法会马上返回nil;
b.如果pointInside方法返回YES,则证明UIWindow可以响应该事件,hitTest方法会接着调用UIWindow子视图的hitTest方法。

  • b1.如果子视图hitTest方法如果有返回视图,则UIWindow的hitTest方法会返回该视图;
  • b2.如果所有子视图hitTest方法都没有返回视图,则UIWindow的hitTest方法会返回自己。

UIWindow是UIView的子类,UIView的hitTest方法实现和上述过程一致。

思考:
UIView在调用子视图hitTest时,是先调用哪些子视图?

从subview数组的末尾开始调用hitTest,subview数组下标越小,视图层级越低。

UIKit确定目标视图后的过程

当UIKit确定目标视图之后,就会创建UITouch,UITouch的window属性和view属性就是上面过程中的UIWindow和目标视图。

接着UIApplication就会调用sendEvent:方法,接着UIWindow在sendEvent:方法中会调用sendTouchesForEvent:方法,如下图:

UIWindow的sendTouchesForEvent:方法调用的是我们熟悉的touches四大方法:
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
从上一步寻找到的目标视图开始,目标视图会首先被调用touches方法,接着是目标视图的父视图,再是父视图的父视图,如果某个视图是ViewController的.view属性,还会调用ViewController的方法,直到UIWindow、UIApplication、UIApplicationDelegate(我们创建的AppDelegate)。

下面是官方文档给出的回调顺序:(Responder chains in an app)

手势处理发生在哪一步

手势(UIGestureRecognizer)是iPhone的重要交互方式,手势识别 介绍了手势是如何识别,甚至可以添加自定义手势。

UIGestureRecognizer同样有touches系列方法:

手势处理的发生时机我们可以通过手势的touchesBegan:withEvent:方法来看,当我们断点在手势的touchesBegan方法时,我们看到堆栈:

注意到堆栈中的UIApplication的sendEvent:方法,sendEvent是发生在UIKit寻找目标视图过程之后。从另外一种角度来思考,touchesBegan方法中会用到UITouch,而UITouch中的view属性是目标视图,所以手势的处理应该也放在UIKit寻找目标视图之后。

当手势的touchesBegan:withEvent:处理完成之后,便会触发目标视图的touchesBegan方法。

但是当手势识别成功之后,默认会cancel后续touch操作,从目标视图开始的响应链都会收到touchesCancelled方法,而不是正常的touchesEnded方法,堆栈如下:

这个行为也可以通过设置下面的cancelsTouchesInView=NO来避免触发touchesCancelled方法。

注意到不管是手势处理开始的touchesBegan方法,还是手势识别成功后触发touchesCancelled方法,堆栈中都有一个UIGestureEnvironment类。这是一个UIKit的私有类,在网上搜到相关代码介绍:

@interface UIGestureEnvironment : NSObject {
    NSMutableArray * _delayedPresses;
    NSMutableArray * _delayedPressesToSend;
    NSMutableArray * _delayedTouches;
    NSMutableArray * _delayedTouchesToSend;
    UIGestureGraph * _dependencyGraph;
    NSMutableArray * _dirtyGestureRecognizers;
    bool  _dirtyGestureRecognizersUnsorted;
    struct __CFRunLoopObserver { } * _gestureEnvironmentUpdateObserver;
    NSMutableSet * _gestureRecognizersNeedingRemoval;
    NSMutableSet * _gestureRecognizersNeedingReset;
    NSMutableSet * _gestureRecognizersNeedingUpdate;
    NSMapTable * _nodesByGestureRecognizer;
    bool  _updateExclusivity;
}

- (void)addGestureRecognizer:(id)arg1;
- (void)addRequirementForGestureRecognizer:(id)arg1 requiringGestureRecognizerToFail:(id)arg2;
- (bool)gestureRecognizer:(id)arg1 requiresGestureRecognizerToFail:(id)arg2;
- (id)init;
- (void)removeGestureRecognizer:(id)arg1;
...

从头文件的方法声明,我们可以大概知道这是一个手势管理类,手势的添加、移除、响应都在内部完成。

思考:

1、UIButton的点击回调是怎么实现的?
2、如果给UIButton添加Tap手势,点击UIButton的时候是触发UIButton的Tap手势,还是触发UIButton的点击回调?

总结

所以综上三步,我们可以知道整个流程大概是:

  1. 寻找目标视图:UIApplication->UIWindow->ViewController->View->targetView
  2. 手势识别:UIGestureEnvironment-> UIGestureRecognizer
  3. 响应链回调:targetView->Viewd->ViewController->UIWindow->UIApplication

iOS的用户交互相关非常复杂。由于时间有限,这里仅仅从事件的传递和处理出发,来建立一个基础的认知。

附录

参考文献

手势识别 https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/implementing_a_custom_gesture_recognizer/about_the_gesture_recognizer_state_machine

响应链介绍 https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events?from=from_parent_mindnote

思考题

1、UIButton的点击回调是怎么实现的?

UIButton是UIControl的子类,通过追踪touch事件的变化得到一些UIControl定义的事件(UIControlEvents);UIButton的点击操作是通过UIControlEvents的事件变化回调来触发,本质依赖的是响应链回调过程中的touches系列方法。

2、如果给UIButton添加Tap手势,点击UIButton的时候是触发UIButton的Tap手势,还是触发UIButton的点击回调?

上文分析了手势的识别是发生在响应链回调之前,也就是tap手势是发生在touches系列方法回调之前,那么Tap手势应该是在UIButton的touches方法之前。如果UIButton监听的是常用的UIControlEventTouchUpInside事件,则不会回调;如果监听的是UIControlEventTouchCancel事件,则在触发完Tap手势之后,还会收到回调。

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

推荐阅读更多精彩内容

  • 在开发过程中,大家或多或少的都会碰到令人头疼的手势冲突问题,正好前两天碰到一个类似的bug,于是借着这个机会了解了...
    闫仕伟阅读 5,138评论 2 23
  • 该文章属于刘小壮原创,转载请注明:刘小壮[https://www.jianshu.com/u/2de707c93d...
    刘小壮阅读 31,889评论 32 209
  • 事件的生命周期 当指尖触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程间通信,事件最终被传递到了合适...
    HughKaun阅读 1,096评论 0 3
  • 1.3事件的传递和处理 (一)事件的产生和传递 事件传递的作用就是找到合适的view来处理事件 1.当发生触摸事件...
    刘2傻阅读 304评论 0 2
  • 在使用手机的过程中,会产生很多交互事件,如触摸屏幕、摇晃、按下按键、使用耳机操控设备等。这些事件都需要系统去响应并...
    pro648阅读 471评论 1 2