zoukankan      html  css  js  c++  java
  • UIResponder响应链

    一、概述
      UIView与UIViewController的共同父类:UIResponder,对于点击touches一系列方法,UIView与UIViewController会做出一系列反应,下面从“如何找到点击的子view”和“如何根据响应链响应”两方面来认识UIResponder。
     
    二、 如何找到点击的子view
      当用户点击某一个视图或者按钮的时候会首先响应application中UIWindow一层一层的向下查找,直到找到用户指定的view为止。
      比如上图,点击ViewE,会首先响应application中UIWindow一层一层的向下查找。查到ViewA,发现点击位置在ViewA内,接下来发现点击位置在ViewB内,接下来发现点击位置在ViewE内,就找到了ViewE。主要通过下面两个方法来找到的:
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;   // default returns YES if point is in bounds

    UIWindow会通过调用hitTest:withEvent:方法(这个方法会对UIWindow的所有子view调用pointInside:withEvent:方法,其中返回的YES的视图为ViewA,得知用户点击的范围在ViewA中),类似地,在ViewA中调用hitTest:withEvent:方法,得知用户点击的范围在ViewB中,依此类推,最终找到点击的视图为ViewE。

    其中,hitTest:withEvent:方法大致的实现如下:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
         for (UIView *view in self.subviews) {
              if([view pointInside:point withEvent:event]){
                  UIView *hitTestView = [view hitTest:point withEvent:event];
                 if(nil == hitTestView){
                      return view;
                  }
              }
          }
         return nil;
     }

    通过以上递归的形式就能找到用户点击的是哪个view,其中还要注意的是当前的view是否开启了userIntercationEnabled属性,如果这个属性未开启,以上递归也会在未开启userIntercationEnabled属性的view层终止。

    三、如何根据响应链响应

      既然找到了用户点击的view,那么当前就应该响应用户的点击事件了,UIView与UIViewController的共同父类是UIResponder,他们都可以复写下列4个方法:

    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
    -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
    -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; 
    -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

      这个响应点击事件的过程是上面的逆序操作,这就是用到了UIResponder的nextResponder方法了。比如上面的图点击ViewE,这时候ViewE先响应,接下来是nextResponder即ViewB,接下来是ViewB的nextResponder即ViewA。

      关于nextResponder有如下几条规则:

    1. 当一个view调用其nextResponder会返回其superView;
    2. 如果当前的view为UIViewController的view被添加到其他view上,那么调用nextResponder会返回当前的UIViewController,而这个UIViewController的nextResponder为view的superView;
    3. 如果当前的UIViewController的view没有添加到任何其他view上,当前的UIViewController的nextResponder为nil,不管它是keyWinodw或UINavigationController的rootViewController,都是如此;
    4. 如果当前application的keyWindow的rootViewController为UINavigationController(或UITabViewController),那么通过调用UINavigationController(或UITabViewController)的nextResponder得到keyWinodw;
    5. keyWinodw的nextResponder为UIApplication,UIApplication的nextResponder为AppDelegate,AppDelegate的nextResponder为nil。

    用图来表示,如下所示:

    四、遇到的问题

      在开发过程中,我们有可能遇到UIScrollView 或 UIImageView 截获touch事件,导致touchesBegan: withEvent:/touchesMoved: withEvent:/touchesEnded: withEvent: 等方法不执行。比如下面这种情况,scrollView的superView是view,view对应的viewController中的touchesBegan: withEvent:/touchesMoved: withEvent:/touchesEnded: withEvent: 等方法就不执行。

    如果想让viewController中的方法执行的话,你可能会提出下面的解决办法:
    @implementation UIScrollView (Touch)
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        if([self isMemberOfClass:[UIScrollView class]]) {
            [[self nextResponder] touchesBegan:touches withEvent:event];
        }
    }
    @end

    这样UIScrollView确实会用nextResponder把响应传递到view,接下来传递到viewController中。但是,如果没有使用if([self isMemberOfClass:[UIScrollView class]]) 进行过滤判断,那么,有可能会导致一个使用系统手写输入法时带来的crash问题。即手写的键盘的子view是UIKBCandidateCollectionView,调用了[[self nextResponder] touchesBegan:touches withEvent:event];后会造成系统的crash问题:

    -[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance 0x104f6c6b0

    这个crash的复现见《UIKBBlurredKeyView candidateList:unrecognized...BUG修复》,它的解决办法也随处可见,比如《-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance 0x5a89960

    此crash的技术层面详细原因:

    手写的键盘的子view是UIKBCandidateCollectionView(UIColloectionView的子类)的实例,它的nextResponder是UIKBHandwritingCandidateView类型的实例,执行UIKBHandwritingCandidateView的touchesBegan:withEvent:方法后,会使得整个candidate view呈选中状态,而苹果对手写键盘的选择candidate字符时的原生处理方法是会避免candidate view呈选中状态的。整个candidate view呈选中状态后后再点击键盘的任意地方,本应调用UIKBCandidateView实例的方法candidateList,结果调用了UIKBBlurredKeyView的candidateList方法,导致方法找不到,导致"-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance "crash。

    crash总结:

    通过对这个crash的详细分析,虽然上面的使用isMemberOfClass判断后使用nextResponder对事件响应链进行传递没有问题,但由于nextResponder依然具有不可控性,还是不建议用category复写系统的方法,这一点以后一定注意。

    谨慎使用Category,特别是覆盖系统原始方法的category的实现。

     (PS:本文参考文章《关于响应者链》)

  • 相关阅读:
    关于SimpleDateFormat安全的时间格式化线程安全问题
    JAVA多线程和并发基础面试问答
    探秘Java中的String、StringBuilder以及StringBuffer
    Java开发岗位面试题归类
    Java并发编程:阻塞队列
    Shell基本概述
    Ansible--06 ansible roles
    Ansible --05 ansible jinja2
    Ansible--04 ansible 流程控制
    Ansible--03 ansible 变量
  • 原文地址:https://www.cnblogs.com/Xylophone/p/7148037.html
Copyright © 2011-2022 走看看