#import "PView.h"
#import <objc/runtime.h>
@implementation PView
+ (void)load {
Class class = [self class];
SEL originalSelector = @selector(touchesBegan:withEvent:);
SEL swizzledSelector = @selector(newTouchesBegan:withEvent:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
- (void)newTouchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"PView newTouchesBegan");
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"PView touchBegan");
为了查看交换效果,先注释SView内的相关方法,然后运行demo,点击红色PView区域,得到log如下:
确定方法已经交换成功。
此时放开SView内的相关方法注释,重新运行项目,分别点击红色PView和绿色SView区域,得到结果:
点击红色PView区域
点击绿色SView区域
看起来没有问题,一切都和按规则推断的一样。
可是真的这样吗?
第4步 调试一下
如果注释绿色SView的touchesBegan方法,会发生什么?
按照响应链规则,事件分发后,由于绿色SView的touchesBegan方法被注释而没有实现,则交由父视图红色PView的touchesBegan方法响应;而红色PView的touchesBegan方法已经与newTouchesBegan交换,因此会先响应红色PView的newTouchesBegan方法。
而绿色SView的touchesEnded方法是实现了的,因此也会响应。
综上,应该是先后响应红色PView的newTouchesBegan方法和绿色SView的touchesEnded方法。
而实际上的运行结果是:
在绿色SView的touchesEnded方法执行完毕后,红色PView的touchesEnded方法也会被执行。
断点再调试一下:
绿色SView的touchesEnded方法的调用堆栈:
红色PView的touchesEnded方法的调用堆栈:
两者相比较,除了红色PView的touchesEnded方法的调用堆栈中间多了一个-[UIResponder _completeForwardingTouches:phase:event:index:]调用外,两者基本一致。
而对于多出来的-[UIResponder _completeForwardingTouches:phase:event:index:]方法调用,目前网上也暂未搜索到有明确的解答。
既然如此,那就多调试几轮:
注释掉PView和SView的touchesBegan方法,保持其它所有方法放开,点击SView,得到的结果是:
注释掉GView,PView和SView的touchesBegan方法,保持其它所有方法放开,点击SView,得到的结果是:
注释掉GView和PView的touchesBegan方法,保持其它所有方法放开,点击SView,得到的结果是:
注释掉全部的touchesBegan方法,保持其它所有方法放开,点击SView,得到的结果是:
注释掉GView和SView的touchesBegan方法,保持其它所有方法放开,点击SView,得到的结果是:
而在此期间我通过断点查看对应View和ViewController是否是第一响应者,得到的结果全部都是NO
假设AppDelegate -> ViewController -> View的关系是树木的根 -> 枝 -> 叶 的关系的话,根据上面调试的结果,我有了初步的总结:
由于点击操作未能确定第一响应者,才会发生touchesEnded方法从叶往根的方向进行递归上抛调用;
touchesEnded的递归上抛调用截止到实现了touchesBegan方法的响应者。
根据UIResponder中的touches相关接口方法的说明,我似乎找到了一种可以信服的说法:
根据接口说明,可以提炼出以下要点:
在自定义点击响应时,应该重写全部四个touches方法;
对于一个响应者来说,如果一个点击操作接收到了touchesBegan:withEvent:事件,那么也会收到同一个点击操作的touchesEnded:withEvent:或touchesCancelled:withEvent:事件;
必须正确的处理点击取消操作,错误的处理可能会导致错误的行为或崩溃。
根据我们实际调试的结果,以及接口说明所提炼的要点,我们可以总结得出以下结论:
对于一个点击操作,如果响应链无法确定一个明确的第一响应者,那么会发生touchesEnded:withEvent:方法的递归上抛;
如果响应者链条中有响应者实现了touchesBegan:withEvent:方法,并且被响应,那么touchesEnded:withEvent:方法的递归上抛截止到该响应者为止;
如果响应者链条中没有响应者实现了touchesBegan:withEvent:方法,那么touchesEnded:withEvent:方法的递归上抛将一直上抛到AppDelegate,然后结束。
同理,如果放开绿色SView的touchesBegan方法,而注释touchesEnded方法,则点击操作响应如下:
可以发现红色PView的任何touches方法都不会执行了。分析如下:
由于绿色SView的touchesBegan方法有实现,而touchesEnded没有重写,因此会执行默认操作,即什么都不做。因此虽然响应链条没有第一响应者,但是touchesEnded的递归上抛也是截止到SView为止。
回顾最开始的面试题,我们可以发现,如果 Swizzle 了 父 View 的 touchesBegan 的方法,对子View没有任何影响。
相比于Swizzle方法,父子视图间响应链问题则更加值得关注。前面总结搬运如下:
对于一个点击操作,如果响应链无法确定一个明确的第一响应者,那么会发生touchesEnded:withEvent:方法的递归上抛;
如果响应者链条中有响应者实现了touchesBegan:withEvent:方法,并且被响应,那么touchesEnded:withEvent:方法的递归上抛截止到该响应者为止;
如果响应者链条中没有响应者实现了touchesBegan:withEvent:方法,那么touchesEnded:withEvent:方法的递归上抛将一直上抛到AppDelegate,然后结束。
参考接口说明,presses相关方法有与touches方法同样需要注意的问题。
参考文档:
调试iOS用户交互事件响应流程
Using Responders and the Responder Chain to Handle Events