某天收到个问题反馈,由于群组新增红包功能,抢红包时点击红包偶现卡顿问题,现象是点击屏幕无响应。
首先分析场景,抢红包的同时不断的接收到新消息,大家可能体会过,眼看一个大红包被新消息顶出屏幕,等到点开的时候就被抢完了。
查看业务逻辑,接受到新消息后会自动滚屏到最新的一条消息。于是不断的收到消息,不断的滚屏。这个过程中用户触摸屏幕,就无响应了。
相关核心代码如下:
/// 实现该方法来监控消息的接收
- (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
点击后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];
}