424,iOS 中事件的响应链和传递链(面试点:1.Runloop 运行循环,监听所有事件,当发生事件后,系统会将事件加入到UIApplication管理的一个任务队列中 2. UIAppli...

谈谈你对事件的传递链和响应链的理解?
  • 一:响应者链 UIResponser包括了各种Touch message 的处理,比如开始,移动,停止等等。常见的 UIResponserUIView及子类,UIViController,APPDelegateUIApplication等等。

回到响应链,响应链是由UIResponser组成的,那么是按照哪种规则形成的。

  • A: 程序启动 UIApplication会生成一个单例,并会关联一个APPDelegateAPPDelegate作为整个响应链的根建立起来,而UIApplication会将自己与这个单例链接,即UIApplicationnextResponser(下一个事件处理者)为APPDelegate

  • B:创建UIWindow 程序启动后,任何的UIWindow被创建时,UIWindow内部都会把nextResponser设置为UIApplication单例。UIWindow初始化rootViewController,rootViewControllernextResponser会设置为UIWindow

  • C:UIViewController初始化 loadView, VCviewnextResponser会被设置为VC.

  • D:addSubView addSubView操作过程中,如果子subView不是VCView,那么subViewnextResponser会被设置为superView。如果是VCView,那就是 subView -> subView.VC ->superView如果在中途,subView.VC被释放,就会变成subView.nextResponser = superView

image.png

我们使用一个现实场景来解释这个问题:当一个用点击屏幕上的一个按钮,这个过程具体发生了什么。

    1. 用户触摸屏幕,系统硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前App进程是两个进程,所以进程两者之间传递事件用的是端口通信。硬件检测进程会将事件放到APP检测的那个端口。
  • 2.APP启动主线程RunLoop会注册一个端口事件,来检测触摸事件的发生。当事件到达,系统会唤起当前APP主线程的RunLoop。来源就是App主线程事件,主线程会分析这个事件。

  • 3.最后,系统判断该次触摸是否导致了一个新的事件, 也就是说是否是第一个手指开始触碰,如果是,系统会先从响应网中 寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个Touch message, 也就是说已经有保存好的响应链

  • 二:事件传递链

通过两种方法来做这个事情。

// 先判断点是否在View内部,然后遍历subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
//判断点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds
复制代码
  • A: 流程
  • 1:先判断该层级是否能够响应(1.alpha>0.01 2.userInteractionEnabled == YES 3.hidden = NO)
  • 2:判断改点是否在view内部,
  • 3:如果在那么遍历子view继续返回可响应的view,直到没有。
    B:常见问题
  • 父view设置为不可点击,子view可以点击吗
  • 不可以,hit test 到父view就截止了
  • 子view设置view不可点击不影响父类点击
  • 同父view覆盖不影响点击
  • 手势对responder方法的影响
  • C:实际用法
  • 点一一个圆形控件,如何实现只点击圆形区域有效,重载pointInside。此时可将外部的点也判断为内部的点,反之也可以。
  • 事件响应链在复杂功能界面进行不同控件间的通信,简便某些场景下优于代理和block

iOS事件链有两条:事件的响应链Hit-Testing事件的传递链

  • 响应链:由离用户最近的view向系统传递。initial view –> super view –> … –> view controller –> window –> Application –> AppDelegate

  • 传递链:由系统向离用户最近的view传递。UIKit –> active app's event queue –>window –> root view –> … –> lowest view

在iOS中只有继承UIResponder的对象才能够接收并处理事件,UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplicationUIViewControllerUIWindow和所有继承自UIViewUIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象,首先我们通过一张图来简单了解一下事件的传递以及响应

image.png
  1. 传递链
  • 事件传递的两个核心方法
- (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;   // def
  • 第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件
  • 第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES
  • 其中UIView不接受事件处理的情况有
1. alpha <0.01
2. userInteractionEnabled = NO
3. hidden = YES
  • 事件传递的流程图
image.png
  • 流程描述
  1. 我们点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow
  2. UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图
  3. hitTest:withEvent:方法中就会去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图
  4. 遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用
  5. 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者
  1. 响应链
  • 响应者链流程图
image.png
  • 响应者链的事件传递过程总结如下
  1. 如果view的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图
  2. 在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给UIWindow对象进行处理
  3. 如果UIWindow对象也不处理,则将事件传递给UIApplication对象
  4. 如果UIApplication也不能处理该事件,则将该事件丢弃
  1. 实例场景
  • 在一个方形按钮中点击中间的圆形区域有效,而点击四角无效
  • 核心思想是在pointInside: withEvent:方法中修改对应的区域
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 如果控件不允许与用用户交互,那么返回nil
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
        return nil;
    }

    //判断当前视图是否在点击范围内
    if ([self pointInside:point withEvent:event]) {
        //遍历当前对象的子视图(倒序)
        __block UIView *hit = nil;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
            CGPoint convertPoint = [self convertPoint:point toView:obj];
            //调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
            hit = [obj hitTest:convertPoint withEvent:event];
            //如果找到了就停止遍历
            if (hit) *stop = YES;
        }];

        //返回当前的视图对象
        return hit?hit:self;
    }else {
        return nil;
    }
}

// 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
- (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;
    }
}

你可能感兴趣的:(424,iOS 中事件的响应链和传递链(面试点:1.Runloop 运行循环,监听所有事件,当发生事件后,系统会将事件加入到UIApplication管理的一个任务队列中 2. UIAppli...)