iOS点击事件响应全过程
小球是一位码农,拿出他心爱的iPhone,解除锁屏,滑动屏幕找到今日头条APP,打开他并点击了其中一篇新闻,进入到了新闻详情页。这时他突然意识到,我从拿出手机到看到新闻详情,具体的实现细节是什么??
屏幕解锁
操作系统层面的,硬件,驱动,系统唤醒
触摸屏幕
- 操作系统层面,硬件驱动处理
- IOKit.framework为系统内核库,封装事件为IOHIDEvent
- 通过mach port转发给SpringBoard.app(IPC进程间通信)。SpringBoard.app可以认为是桌面
- 如果当前手机就在桌面,例如用户滑屏,或是点击App icon,交由系统消化处理
- 当前存在活跃的APP,继续通过mach port转发,给到当前APP主线程
- 触发APP进程内,runloop中处理的Source1回调
- Source1触发Source0回调,将IOHIDEvent封装为UIEvent
- Source0调用UIApplication的sendEvent,将UIEvent传递给UIWindow
- 响应链逻辑:事件传递链和事件响应链
总结来说:触摸屏幕的整个过程如上图,硬件驱动唤醒系统的runloop,进程间通信传递到APP中,APP的runloop被唤醒,开始调动查找第一响应者,响应者通过消息转发机制找到响应的方法并执行。
响应链
能响应的控件,都是继承至UIResponder
UI继承结构
先着眼UIResponder,看看他和其他的同一层的兄弟类有什么区别,比如他和UIScreen、UIColor的区别。进入UIResonder.h
文件,一眼就能看见,属性方法都是和用户的操作有关的内容,其中主要内容如下
- responder相关,
nextResponder
、becomeFirstResponder
、resignFirstResponder
、isFirstResponder
- touch相关,began、moved、ended、cancelled等
- press相关,began、changed、ended、cancelled等
- motion相关,began、ended、cancelled等
UIView和UIButton的区别
同样都是继承至UIResponder,这两个又有什么区别呢,为什么view不能点击呢?
首先,需要了解他们的继承关系:
NSObject
->UIRespopnder
->UIView
->UIControl
->UIButton
然后我们在看看UIView.h
、UIControl.h
、UIButton.h
三个文件,很容易得出结论:
- UIView是UIButton的父类
- UIView的关键方法为:
hitTest:withEvent:
和pointInside:withEvent:
- UIControl的关键方法:
addTarget:action:forControlEvents:
- UIButton中的内容都是有关按钮控件界面的东西了
- UIView和CALayer的关系和区别?
- UIView中的frame和bounds的关系和区别?
查找第一响应者
iOS事件链有两条:Hit-Testing
事件的传递链、事件的响应链:
传递链:由系统向离用户最近的view传递。
UIKit
–>active app's event queue
–>window
–>root view
–> …… –>lowest view
响应链:由离用户最近的
view
向系统传递。initial view
–>super view
–> ….. –>view controller
–>window
–>Application
–>AppDelegate
事件传递链
事件传递的两个核心方法
1 2
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
详细实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
-(UIView *)hitTes:(CGPoint)point withEvent:(UIEvent *)event { //判断自己是否能接受事件 if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) { return nil: } // 判断当前点 在不在自己身上. if (![self pointInside:point withEvent:event]) { return nil; } // 查看自己是不是最合适的view,从后往前遍历自己的子控件. int count = (int)self.subviews.count; for (int i = count -1 ; i >= 0; i--) { UIView *childView = self.subviews[i]; CGPoint childP = [self convertPoint:point toView:childView]; UIView *view = [childView hitTest:childP withEvent:event]; if (view) { return view; } } return self; } // 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { CGFloat x1 = point.x; CGFloat y1 = point.y; CGFloat x2 = self.frame.size.width / 2; CGFloat y2 = self.frame.size.height / 2; //判断是否在圆形区域内 double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); if (dis <= self.frame.size.width / 2) { return YES; } else { return NO; } }
第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件
第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES
其中UIView不接受事件处理的情况有
- alpha <0.01
- userInteractionEnabled = NO
- hidden = YES
流程概述
- 我们点击屏幕产生触摸事件,系统将这个事件加入到一个由
UIApplication
管理的事件队列中,UIApplication
会从消息队列里取事件分发下去,首先传给UIWindow
- 在
UIWindow
中就会调用hitTest:withEvent:
方法去返回一个最终响应的视图 - 在
hitTest:withEvent:
方法中就会去调用pointInside: withEvent:
去判断当前点击的point
是否在UIWindow
范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图 - 遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的
hitTest:withEvent:
方法,可以理解为是一个递归调用 - 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将
UIWindow
作为响应者
- 我们点击屏幕产生触摸事件,系统将这个事件加入到一个由
事件响应链
响应者链的事件传递过程总结如下
- 响应链是通过nextResponder属性组成的链表
- 在事件的响应中,如果某个控件实现了
touches...
方法,则这个事件将由该控件来接受,如果调用了[super touches….]
;就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….
方法。 - 如果
view
的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图 - 在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给
UIWindow
对象进行处理 - 如果
UIWindow
对象也不处理,则将事件传递给UIApplication
对象 - 如果
UIApplication
也不能处理该事件,则将该事件丢弃
如果button上面叠加添加了手势,手势会覆盖住不执行target方法,除非设置手势的cancelsTouchesInView = NO
如果button上有subview,subview需要设置userInteractionEnabled=NO https://segmentfault.com/q/1010000004153263
通过在
Button子类
中重写touches
的方法,发现如果不调用super
的touches
对应的方法则不会响应点击事件。由此可以大致推断出UIControl其子类
响应点击原理大致为:根据添加target:action:
时设置的UIControlEvents
,在touches
的合适方法调用target的action
方法。通过重写
tableView子类
的touches
方法,发现如果不调用super
的touches
对应的方法则不会走tableview:didSelectRowAtIndexPath:
方法。由此可以大致推断出UIScrollView
其子类是在其touches
方法中处理点击事件的。gestureRecognizer优先级更高 手势和touch的关系
响应链应用示例
- 扩大
Button
的点击区域
1
2
3
4
5
6
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(CGRectInset(self.bounds, -20, -20), point)) {
return YES;
}
return NO;
}
子
view
超出了父view
的bounds
响应事件1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { BOOL flag = NO; for (UIView *view in self.subviews) { if (CGRectContainsPoint(view.frame, point)){ flag = YES; break; } } return flag; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) { return nil; } if ([self pointInside:point withEvent:event]) { for (UIView *subview in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; }
如果Button被一个View遮住,在触摸View时,希望该Button能够响应事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// in View.m里 // 击View及View的非交互子View(例如UIImageView),则该Button可以响应事件 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { BOOL next = YES; for (UIView *view in self.subviews) { if ([view isKindOfClass:[UIControl class]]) { if (CGRectContainsPoint(view.frame, point)){ next = NO; break; } } } return !next; } // in View.m里 // 点击View本身Button会响应该事件,点击View的任何一个子View,Button不会响应事件 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *view = [super hitTest:point withEvent:event]; if (view == self) { return nil; } return view; }