iOS 事件传递与响应链原理

一 iOS中的事件

1 事件的产生和类型

用户对iOS设备进行了一些操作, 比如点击屏幕、滑动屏幕, 摇晃设备, 拖拽图片, 放大图片, 远程控制(蓝牙)等等, 这些操作生成了事件(UIEvent). 事件被官方根据具体操作性质被分成了若干事件类型,包括

typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches,			// 触摸事件
    UIEventTypeMotion,			// 运动事件
    UIEventTypeRemoteControl,	// 远程控制事件
    UIEventTypePresses API_AVAILABLE(ios(9.0)),	// 按压事件
};

下面我们只讲述触摸事件, 触摸事件由以下几个API产生

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 触摸移动
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 触摸开始
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 触摸结束
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 触摸取消
}

2 响应者对象

在 iOS 中, 不是所有对象都可以响应事件, 只有继承了 UIReponder(响应者对象)类的对象才能接收并处理事件. 包括 UIView 、UIViewController 和 UIApplication 及它们的子类(UILabel不响应事件)

Responder objects—that is, instances of UIResponder—constitute the event-handling backbone of a UIKit app. Many key objects are also responders, including the UIApplication object, UIViewController objects, and all UIView objects (which includes UIWindow). As events occur, UIKit dispatches them to your app’s responder objects for handling.

3 UIview和CALayer

我们在屏幕上能看到的所有内容载体都是或者继承自UIView. UIView内部有一个只读的CALayer类型属性.

// returns view's layer. Will always return a non-nil value. view is layer's delegate
@property(nonatomic,readonly,strong)                 CALayer  *layer;              

UIView和CALayer在干活的时候, 有着不同的分工, 两者各司其职, 互不干扰!通俗一点讲, 就是CALayer只负责显示, 其他的归UIView管, 这里体现了六大设计原则中的单一职责原则

UIview

  • 负责为CALayer提供内容;
  • 负责处理事件传递;
  • 负责参与响应链;

CALayer

  • 负责对UIView提供的内容进行渲染和绘制

二 事件传递和响应机制

1 事件的传递

事件的传递是自顶向下的(下指子视图)
1.1 屏幕产生触摸事件, 系统将事件捕捉到UIApplication管理的一个任务队列中;
1.2 UIApplication将任务队列最前端的事件向下传递分发给住窗口(keyWindow)
1.3 主窗口判断当前触摸事件是否在当前屏幕范围内, 如果是继续把向下分发, 递归倒序调用所有子视图, 以找到合适的响应者来处理触摸事件; 如果不在, 则自身作为响应者;

响应者对象会根据下面两个API判断是否可以自己是否可以作为响应者,如果是, 倒序递归遍历所有子视图, 寻找响应者

// 返回响应的视图
// 我们可以在这个方法内部可以根据需求改变响应视图, 做事件拦截
 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {return nil;}
// 判断某个点击位置是否在当前视图范围内
// 此方法重载返回NO, 则此视图不再响应事件
 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {return NO;}

2 系统实现

所有子视图内部同样调用上述两个方法,倒序遍历自身的所有子视图, 调用机制如下图, 也就是说最后添加的视图最先被遍历事件传递流程
hitTest:withEvent内部实现, 视图如果想事件的传递, 需要同时满足以下几个条件

  • hidden != YES;
  • userInteractionEnable属性 = yes 即允许用户交互;
  • alpha > 0.01 即透明度大于0.01;
  • 未超出父视图区域;

视图遍历步骤,图片来源于网络

3 视图的响应

继承自UIReponder的响应者对象接受到事件后, 开始自下向上寻找响应者(下为子视图)

  • 顶层view接受到触摸事件
  • 如果该view有所属vc,则传递给vc,否则传递给其superView
  • 如果VC有其父VC,继续传递给父VC直至UIWindow, superview一样
  • UIWindow继续传递给第一响应者UIApplication
  • UIApplication传递给UIApplicationDelegate
    下图是苹果官方文档的一幅路程图, 图片在这里. UIlabel等是不可以响应事件的

If the text field does not handle an event, UIKit sends the event to the text field’s parent UIView object, followed by the root view of the window. From the root view, the responder chain diverts to the owning view controller before directing the event to the window. If the window cannot handle the event, UIKit delivers the event to the UIApplication object, and possibly to the app delegate if that object is an instance of UIResponder and not already part of the responder chain.

在这里插入图片描述

三 事件和runloop

我们前面讲到, 当用户触摸屏幕后, 触发了一个触摸事件, 该事件被UIApplication捕获到一个事件队列中, 这个队列就是当前程序运行的主队列, 即主runloop事件循环.我们都知道, runloop在有事做的时候被唤醒, 没事做的时候休眠! 当事件添加到队列后, runloop立即被唤醒, 并通过mach_msg,mach_port等一系列函数的调用, 实现内核态到用户态的切换.具体runloop的实现机制大家可以下载苹果开源代码查看.

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                  mach_msg(msg, MACH_RCV_MSG, port); 
                  // thread wait for receive msg
}
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);

runloop被唤醒后, 开始处理用户的触摸事件, 保证当前事件处理过程中, 程序不会退出(主线程不会退出), 同时对处理过程进行监听, 直到事件被处理完毕.
这是很久很久之前我在网上找的一张图, 现在找不到出处了-.-, 若有侵犯, 请联系告知我删除
运行循环监听所有事件
我是个写代码的小学生, 如有不足, 万望不吝指教

参考资料:
运行循环
响应和处理事件的抽象接口
UIKit Apps事件处理指南
史上最详细的iOS之事件的传递和响应机制

发布了4 篇原创文章 · 获赞 1 · 访问量 97

猜你喜欢

转载自blog.csdn.net/weixin_43837354/article/details/104648709