从点击屏幕无响应问题说起

某天收到个问题反馈,由于群组新增红包功能,抢红包时点击红包偶现卡顿问题,现象是点击屏幕无响应。

首先分析场景,抢红包的同时不断的接收到新消息,大家可能体会过,眼看一个大红包被新消息顶出屏幕,等到点开的时候就被抢完了。

查看业务逻辑,接受到新消息后会自动滚屏到最新的一条消息。于是不断的收到消息,不断的滚屏。这个过程中用户触摸屏幕,就无响应了。

相关核心代码如下:

/// 实现该方法来监控消息的接收
- (void)receiveNewMessage
{
    [self.tableView scrollToBottomAnimated:YES];
}

/// scrollToBottomAnimated: 方法实现如下
- (void)scrollToBottomAnimated:(BOOL)animated {
    NSUInteger finalRow = MAX(0, [self numberOfRowsInSection:0] - 1);
    NSIndexPath *finalIndexPath = [NSIndexPath indexPathForRow:finalRow inSection:0];
    
    [self scrollToRowAtIndexPath:finalIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:animated];
}

问题来了,是什么原因造成无响应的?

一开始以为是滚屏的动作大量占用CPU时间,造成无法响应用户的触控事件。用Mock代码模拟了下不断收到新消息时点击红包的场景,发现除了触摸 tableView 不能响应外,其它非tableView的区域还是可以响应触控事件的。可见并非是这个原因。

由于收到消息滚屏到底部的过程是有动画效果的,不断的滚屏,动画是持续生效的。UIView默认执行动画时不响应触控事件的。试着去掉动画效果,直接滑动底部,就能响应用户的触控事件了。

- (void)scrollToBottomAnimated:(BOOL)animated {
    NSUInteger finalRow = MAX(0, [self numberOfRowsInSection:0] - 1);
    NSIndexPath *finalIndexPath = [NSIndexPath indexPathForRow:finalRow inSection:0];
    
    [UIView animateKeyframesWithDuration:animated?0.3:0 delay:0 options:UIViewKeyframeAnimationOptionAllowUserInteraction | UIViewKeyframeAnimationOptionBeginFromCurrentState animations:^{
        [self scrollToRowAtIndexPath:finalIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
    } completion:^(BOOL finished) {
        
    }];
}

按上述滚屏逻辑即可响应用户的触控事件,可见是这个原因。

修复了这个问题,测试中发现偶尔还是点击后无响应。用户的触控事件应该已经传给了UIWindow,难道是没有传给应该响应事件的View?

Hook下UIWindow的sendEvent:方法,观察下事件的传递:

@implementation UIWindow (KeyWindow)

+ (void)load
{
    [self hookSwizzleSelector:@selector(sendEvent:) withSelector:@selector(y_sendEvent:)];
}

- (void)y_sendEvent:(UIEvent *)event
{
    [self y_sendEvent:event];
}

@end
image.png

点击后event如上图所示,可见事件有传递给View,观察到同时有Tap和Long事件,猜测是由于Long事件致使Tap事件失效导致的。查看代码后发现在父类中有添加Long事件:

        UILongPressGestureRecognizer *contentLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
        contentLongPress.minimumPressDuration = 0.5;
        [self.contentView addGestureRecognizer:contentLongPress];

修改后如下:

// 添加单击手势:
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureResponse:)];
    tapGesture.delegate = self;
    [[self.contentView gestureRecognizers] enumerateObjectsUsingBlock:^(__kindof UIGestureRecognizer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[UILongPressGestureRecognizer class]]) {
            [tapGesture requireGestureRecognizerToFail:obj];
        }
    }];
    [self.bubbleImageView addGestureRecognizer:tapGesture];
    
// 处理手势冲突,响应单击手势时,不响应其它手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        return YES;
    }
    return NO;
}

还有优化的空间吗?指定执行滚屏时的runloop为NSDefaultRunLoopModel,用户触摸屏幕时runloop会切换为EventTracking,理论上这样应该会优先响应触控事件。但测试中发现区别不大,欢迎大家讨论 这样做是否能优先响应触控事件?

- (void)receiveNewMessage
{
    [self performSelector:@selector(receiveOnlineMessageScrollToBottom) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
    
}

- (void)receiveOnlineMessageScrollToBottom
{
    [self.tableView scrollToBottomAnimated:YES];
}

你可能感兴趣的:(从点击屏幕无响应问题说起)