原文地址
背景
在一般的開(kāi)發(fā)場(chǎng)景中,很難會(huì)遇到 UIButton 響應(yīng)延遲的問(wèn)題。就算遇到大多數(shù)都是因?yàn)橹骶€程阻塞,這種問(wèn)題很容易解決,接下來(lái)說(shuō)說(shuō)另外一種非常規(guī)延遲。
栗子
測(cè)試主要代碼如下:
#import "MyButton.h"
@implementation MyButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
return [super hitTest:point withEvent:event];
}
@end
#import "ViewController.h"
#import "MyButton.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet MyButton *aButton;
@property (weak, nonatomic) IBOutlet UIView *aView;
@property (assign, nonatomic) BOOL isAnimating;
@property (strong, nonatomic) UITapGestureRecognizer *doubleTapGesture;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.aView.layer.cornerRadius = self.aView.frame.size.width * 0.5;
self.doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onDoubleTap:)];
// self.doubleTapGesture.delaysTouchesEnded = NO;
// self.doubleTapGesture.delegate = self;
self.doubleTapGesture.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:self.doubleTapGesture];
}
- (void)onDoubleTap:(UITapGestureRecognizer *)doubleTapGesture
{
NSLog(@"%s", __func__);
}
- (IBAction)onButtonClicked:(MyButton *)sender
{
NSLog(@"%s", __func__);
if (self.isAnimating) {
return;
}
self.isAnimating = YES;
[self startAnimation];
}
- (void)startAnimation
{
self.aView.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:0.5 animations:^{
self.aView.transform = CGAffineTransformMakeScale(1.3, 1.3);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
self.aView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
self.isAnimating = NO;
}];
}];
}
@end
場(chǎng)景一:UIButton
注釋掉手勢(shì)代碼,點(diǎn)擊按鈕,日志輸出如下:
2019-11-15 10:47:28.677167+0800 UIButtonDelayDemo[74007:2703683] -[MyButton hitTest:withEvent:]
2019-11-15 10:47:28.677383+0800 UIButtonDelayDemo[74007:2703683] -[MyButton hitTest:withEvent:]
2019-11-15 10:47:28.753206+0800 UIButtonDelayDemo[74007:2703683] -[ViewController onButton1Clicked:]
結(jié)論: Button 響應(yīng)時(shí)間間隔: 76ms
場(chǎng)景二:UIButton 與 單擊 UITapGestureRecognizer
改為單擊手勢(shì) self.doubleTapGesture.numberOfTapsRequired = 1; ,點(diǎn)擊按鈕,日志輸出如下:
2019-11-15 10:52:34.546937+0800 UIButtonDelayDemo[73697:2687202] -[MyButton hitTest:withEvent:]
2019-11-15 10:52:34.547177+0800 UIButtonDelayDemo[73697:2687202] -[MyButton hitTest:withEvent:]
2019-11-15 10:52:34.636282+0800 UIButtonDelayDemo[73697:2687202] -[ViewController onButton1Clicked:]
結(jié)論: 從 Button 被確定為第一響應(yīng)者到點(diǎn)擊事件響應(yīng)的時(shí)間間隔: 89ms,點(diǎn)擊正常響應(yīng),UI表現(xiàn)無(wú)延遲
場(chǎng)景三:UIButton 與 雙擊 UITapGestureRecognizer
改為雙擊手勢(shì) self.doubleTapGesture.numberOfTapsRequired = 2; ,點(diǎn)擊按鈕,日志輸出如下:
2019-11-15 11:00:30.294756+0800 UIButtonDelayDemo[73944:2700167] -[MyButton hitTest:withEvent:]
2019-11-15 11:00:30.295125+0800 UIButtonDelayDemo[73944:2700167] -[MyButton hitTest:withEvent:]
2019-11-15 11:00:30.740430+0800 UIButtonDelayDemo[73944:2700167] -[ViewController onButton1Clicked:]
結(jié)論: 從 Button 被確定為第一響應(yīng)者到點(diǎn)擊事件響應(yīng)的時(shí)間間隔為: 445ms,點(diǎn)擊異常響應(yīng),UI表現(xiàn)延遲明顯。為什么 UIButton 的 superView 上存在雙擊手勢(shì)時(shí)會(huì)造成其響應(yīng)延時(shí)呢?原因如下。(P.s:以上場(chǎng)景的響應(yīng)時(shí)間間隔跟多次測(cè)試取平均的結(jié)果相近,可自行驗(yàn)證)
分析
UIButton 事件響應(yīng)延遲具體原因如下:
1、手勢(shì)的事件響應(yīng)優(yōu)先級(jí)比 UIButton 高。官方文檔:Using Responders and the Responder Chain to Handle Events
Gesture recognizers receive touch and press events before their view does. If a view's gesture recognizers fail to recognize a sequence of touches, UIKit sends the touches to the view. If the view does not handle the touches, UIKit passes them up the responder chain
2、雙擊手勢(shì)會(huì)將 touch event 掛起一段時(shí)間(時(shí)間大概在300 ~ 400ms之間)用來(lái)分析其是否位雙擊事件,只有手勢(shì)分析觸摸事件結(jié)束之后才將掛起的 touch event 傳給 View。此外, UIGestureRecognizer 的 delaysTouchesEnded 屬性說(shuō)明了一切
default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
@property(nonatomic) BOOL delaysTouchesEnded;
Discussion
When the value of this property is YES (the default) and the receiver is analyzing touch events, the window suspends delivery of touch objects in the UITouchPhaseEnded phase to the attached view. If the gesture recognizer subsequently recognizes its gesture, these touch objects are cancelled (via a touchesCancelled:withEvent: message). If the gesture recognizer does not recognize its gesture, the window delivers these objects in an invocation of the view’s touchesEnded:withEvent: method. Set this property to NO to have touch objects in the UITouchPhaseEnded delivered to the view while the gesture recognizer is analyzing the same touches.
UIButton 事件響應(yīng)調(diào)用棧差異
對(duì)比可知:?jiǎn)?雙擊中的事件傳遞是不一樣的,雙擊手勢(shì)調(diào)用棧中多了一些手勢(shì)相關(guān)的調(diào)用 _UIGestureEnvironmentSortAndSendDelayedTouches:
單擊手勢(shì)

雙擊手勢(shì)

解決
由上面的分析可知:手勢(shì)會(huì)掛起觸摸事件直到其分析結(jié)束后才將事件繼續(xù)傳遞給 View,那么只需要將 delaysTouchesEnded 設(shè)置為 NO 即可,這樣子在雙擊手勢(shì)分析觸摸事件期間就可以把該事件傳給對(duì)應(yīng)的 View,互不干擾。代碼如下:
self.doubleTapGesture.delaysTouchesEnded = NO;
設(shè)置后的按鈕響應(yīng)正常,日志打印如下:
2019-11-15 17:24:05.459630+0800 UIButtonDelayDemo[1458:416963] -[MyButton hitTest:withEvent:]
2019-11-15 17:24:05.460181+0800 UIButtonDelayDemo[1458:416963] -[MyButton hitTest:withEvent:]
2019-11-15 17:24:05.526318+0800 UIButtonDelayDemo[1458:416963] -[ViewController onButtonClicked:]
設(shè)置為 NO 后,雙擊按鈕,會(huì)出現(xiàn)按鈕和雙擊手勢(shì)同時(shí)響應(yīng)的問(wèn)題。如果雙擊手勢(shì)的響應(yīng)和按鈕點(diǎn)擊之間有沖突,那么還需要判斷下改手勢(shì)是否可以接收該 touch。代碼如下:
self.doubleTapGesture.delegate = self;
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isEqual:self.aButton]) {
return NO;
}
return YES;
}
結(jié)尾
本文到此就結(jié)束了,雖然這是一個(gè)很簡(jiǎn)單的問(wèn)題,但是業(yè)務(wù)上的復(fù)雜可能使其變成一個(gè)復(fù)雜的問(wèn)題(P.s:當(dāng)然大牛除外)。當(dāng)時(shí)這個(gè)問(wèn)題花了將近一天的時(shí)間去排查。最終發(fā)現(xiàn)原因之后慨嘆:官方文檔還需多看看?。。∵@篇文章就算是沒(méi)有好好閱讀的官方文檔的檢討書(shū)吧。
測(cè)試代碼:UIButtonDelayDemo