美文网首页
(八)解释栗子

(八)解释栗子

作者: 小白猿 | 来源:发表于2019-03-01 17:29 被阅读0次

    本文系转载,原文地址为iOS触摸事件全家桶

    现在,把胶卷回放到本章节开头的场景。给你一杯咖啡的时间看看能不能解释得通那几个现象了,不说了泡咖啡去了...

    我肥来了!

    先看现象二,短按 cell无法响应,日志如下:

    -[GLTableView touchesBegan:withEvent:]
    backview taped
    -[GLTableView touchesCancelled:withEvent:]
    
    

    这个日志和上面离散型手势Demo中打印的日志完全一致。短按后,BackView上的手势识别器先接收到事件,之后事件传递给hit-tested view,作为响应者链中一员的GLTableView的 touchesBegan:withEvent: 被调用;而后手势识别器成功识别了点击事件,action执行,同时通知Application取消响应链中的事件响应,GLTableView的 touchesCancelled:withEvent: 被调用。

    因为事件被取消了,因此Cell无法响应点击。

    再看现象三(长按Cell),长按cell能够响应,日志如下:

    -[GLTableView touchesBegan:withEvent:]
    -[GLTableView touchesEnded:withEvent:]
    cell selected!
    
    

    长按的过程中,一开始事件同样被传递给手势识别器和hit-tested view,作为响应链中一员的GLTableView的 touchesBegan:withEvent: 被调用;此后在长按的过程中,手势识别器一直在识别手势,直到一定时间后手势识别失败,才将事件的响应权完全交给响应链。当触摸结束的时候,GLTableView的 touchesEnded:withEvent: 被调用,同时Cell响应了点击。

    OK,现在回到现象一(快速点击cell)。按照之前的分析,快速点击cell,讲道理不管是表现还是日志都应该和现象二一致才对。然而日志仅仅打印了手势识别器的action执行结果。分析一下原因:GLTableView的 touchesBegan 没有调用,说明事件没有传递给hit-tested view。那只有一种可能,就是事件被某个手势识别器拦截了。目前已知的手势识别器拦截事件的方法,就是设置 delaysTouchesBegan 为YES,在手势识别器未识别完成的情况下不会将事件传递给hit-tested view。然后事实上并没有进行这样的设置,那么问题可能出在别的手势识别器上。

    Window的 sendEvent: 打个断点查看event上的touch对象维护的手势识别器数组:

    image

    捕获可疑对象:UIScrollViewDelayedTouchesBeganGestureRecognizer ,光看名字就觉得这货脱不了干系。从类名上猜测,这个手势识别器大概会延迟事件向响应链的传递。github上找到了该私有类的头文件

    @interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
        UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client;
        struct CGPoint { 
            float x; 
            float y; 
        }  _startSceneReferenceLocation;
        UIDelayedAction * _touchDelay;
    }
    - (void).cxx_destruct;
    - (id)_clientView;
    - (void)_resetGestureRecognizer;
    - (void)clearTimer;
    - (void)dealloc;
    - (void)sendDelayedTouches;
    - (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
    - (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
    - (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
    - (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
    - (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
    - (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
    @end
    
    

    有一个_touchDelay变量,大概是用来控制延迟事件发送的。另外,方法列表里有个 sendTouchesShouldBeginForDelayedTouches: 方法,听名字似乎是在一段时间延迟后向响应链传递事件用的。为一探究竟,我创建了一个类hook了这个方法:

    //TouchEventHook.m
    + (void)load{
        Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer");
        SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:);
        Method method = class_getClassMethod([self class], sel);
        class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method));
        exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
    }
    
    - (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{
        [self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
    }
    
    void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {
        Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
        Method newMethod = class_getInstanceMethod(aClass, newSEL);
        method_exchangeImplementations(oldMethod, newMethod);
    }
    
    

    断点看一下点击cell后 hook_sendTouchesShouldBeginForDelayedTouches: 调用时的信息:

    image

    可以看到这个手势识别器的 _touchDelay 变量中,保存了一个计时器,以及一个长得很像延迟时间间隔的变量m_delay。现在,可以推测该手势识别器截断了事件并延迟0.15s才发送给hit-tested view。为验证猜测,我分别在Window的 sendEvent:hook_sendTouchesShouldBeginForDelayedTouches: 以及TableView的 touchesBegan: 中打印时间戳,若猜测成立,则应当前两者的调用时间相差0.15s左右,后两者的调用时间很接近。短按Cell后打印结果如下(不能快速点击,否则还没过延迟时间触摸就结束了,无法验证猜测):

    -[GLWindow sendEvent:]调用时间戳 :
    525252194779.07ms
    -[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]调用时间戳 :
    525252194930.91ms
    -[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]调用时间戳 :
    525252194931.24ms
    -[GLTableView touchesBegan:withEvent:]调用时间戳 :
    525252194931.76ms
    
    

    因为有两个 UIScrollViewDelayedTouchesBeganGestureRecognizer,所以 hook_sendTouchesShouldBeginForDelayedTouches 调了两次,两次的时间很接近。可以看到,结果完全符合猜测。

    这样就都解释得通了。现象一由于点击后,UIScrollViewDelayedTouchesBeganGestureRecognizer 拦截了事件并延迟了0.15s发送。又因为点击时间比0.15s短,在发送事件前触摸就结束了,因此事件没有传递到hit-tested view,导致TableView的 touchBegin 没有调用。而现象二,由于短按的时间超过了0.15s,手势识别器拦截了事件并经过0.15s后,触摸还未结束,于是将事件传递给了hit-tested view,使得TableView接收到了事件。因此现象二的日志虽然和离散型手势Demo中的日志一致,但实际上前者的hit-tested view是在触摸后延迟了约0.15s左右才接收到触摸事件的。

    至于现象四 ,你现在应该已经觉得理所当然了才对。

    (九)结语

    相关文章

      网友评论

          本文标题:(八)解释栗子

          本文链接:https://www.haomeiwen.com/subject/xicjuqtx.html