iOS UIGestureRecognizer与原生响应机制处理流程分析

2024-04-26 02:38

本文主要是介绍iOS UIGestureRecognizer与原生响应机制处理流程分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

之前在组内分享的时候,曾提到过在iOS事件响应机制中,当原生触摸事件与手势搭配时,相关方法的调用顺序。之前是将手势也理解至响应者链中,后来发现理解有误,所以在此进行一些总结。

UIGestureRecognizer与原生触摸事件均为处理用户点击事件,所以两者必然存在着紧密关联,所以在探究UIGestureRecognizer响应机制之前,我们先了解一下原生触摸事件的响应机制。

由于iOS系统的封闭性,我们在探究时可能需要做一些方法的重写、打点以及一些方法栈的打印。

以下分析是基于iOS 13.3模拟器进行的。

一、原生触摸事件响应机制

1、系统处理

点击事件是如何产生的?以及该事件是如何交给我们的App呢?

这些都属于系统处理,大致流程如下:

  1. 屏幕硬件捕获用户点击,由 IOKit封装为 IOHIDEvent对象,将该对象交由桌面 SpringBoard.app
  2. 桌面 SpringBoard.app获取当前处于前台的App,将收到的 IOHIDEvent对象发送给该App。

2、App处理

App处理过程其实网上已经做过很多次的讨论和总结了,大致过程如下:

  1. 当由 SpringBoard.app通知App时,本质上是由一个source1类型的事件唤起当前runloop,并触发 __IOHIDEventSystemClientQueueCallback回调。
  2. source1触发source0,在source0回调中,处理收到的 IOHIDEvent对象,将之封装为 UIEvent对象。
  3. App开始我们常说的寻找响应者过程,即 Hit-Testing,该过程会寻找到一系列可处理当前时间的控件。
  4. 由上一步获取到的响应者链开始依次处理事件,若事件在某个节点被处理了,那么整个过程就结束了;若事件一直未被处理,那么该事件就会被抛弃。

3. Hit-Testing

Hit-Testing目的为确定当前点击点所处视图链。

该过程主要依赖以下两个方法:

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

我们可以搭建以下结构的视图,并重写UIView的这两个方法,通过点击E视图来了解Hit-Testing的工作原理。

在这里插入图片描述

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{NSLog(@"进入 View %@ 的 hitTest:withEvent:", self.name);UIView *view = [super hitTest:point withEvent:event];NSLog(@"离开 View %@ 的 hitTest:withEvent: %@", self.name, [view isKindOfClass:[CustomView class]] ? ((CustomView *)view).name : @"");return view;
}- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{NSLog(@"进入 View %@ 的 pointInside:withEvent:", self.name);BOOL isInside = [super pointInside:point withEvent:event];NSLog(@"离开 View %@ 的 pointInside:withEvent: %@", self.name, @(isInside));return isInside;
}

输出如下:

2020-02-19 11:22:29.909200+0800 TouchDemo[22486:1782621] 进入 View A 的 hitTest:withEvent:
2020-02-19 11:22:29.909434+0800 TouchDemo[22486:1782621] 进入 View A 的 pointInside:withEvent:
2020-02-19 11:22:29.909626+0800 TouchDemo[22486:1782621] 离开 View A 的 pointInside:withEvent: 1
2020-02-19 11:22:29.909772+0800 TouchDemo[22486:1782621] 进入 View C 的 hitTest:withEvent:
2020-02-19 11:22:29.909986+0800 TouchDemo[22486:1782621] 进入 View C 的 pointInside:withEvent:
2020-02-19 11:22:29.910132+0800 TouchDemo[22486:1782621] 离开 View C 的 pointInside:withEvent: 1
2020-02-19 11:22:29.910271+0800 TouchDemo[22486:1782621] 进入 View E 的 hitTest:withEvent:
2020-02-19 11:22:29.910418+0800 TouchDemo[22486:1782621] 进入 View E 的 pointInside:withEvent:
2020-02-19 11:22:29.910561+0800 TouchDemo[22486:1782621] 离开 View E 的 pointInside:withEvent: 1
2020-02-19 11:22:29.910715+0800 TouchDemo[22486:1782621] 离开 View E 的 hitTest:withEvent: E
2020-02-19 11:22:29.910862+0800 TouchDemo[22486:1782621] 离开 View C 的 hitTest:withEvent: E
2020-02-19 11:22:29.911060+0800 TouchDemo[22486:1782621] 离开 View A 的 hitTest:withEvent: E

有输出不难分析出,系统会通过调用hitTest:withEvent来返回一个可以响应当前事件的控件,该控件可以为自己,也可以为子控件,而pointInside:withEvent:方法则返回当前控件是否可以响应事件。

另外,在绝大多数情况下,这一套流程会触发两次,官方给出的解释如下:

Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.

即为了避免在正式处理事件之前做了对View的调整,会再进行确认,而该确认没有多余的副作用。

至此,可以处理事件的响应者链就确定了,该响应者链对应一个点击,理应应当被该点击所知,但是我们在这两个方法中打印一下UIEvent,发现其中并没有对应的UITouch,那么UITouch是在何时创建的,我们可以hook UITouch的init方法来确认一下。

在此之前,我们先获取一下在确定响应者链时的方法栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1* frame #0: 0x000000010feee454 TouchDemo`-[CustomView hitTest:withEvent:](self=0x00007ff79650a650, _cmd="hitTest:withEvent:", point=(x = 168, y = 783), event=0x00006000009e0000) at CustomView.m:30:53frame #1: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87frame #2: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121frame #3: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7frame #4: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524frame #5: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482frame #6: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87frame #7: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121frame #8: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7frame #9: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524frame #10: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482frame #11: 0x00007fff4851de63 UIKitCore`-[UIDropShadowView hitTest:withEvent:] + 242frame #12: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87frame #13: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121frame #14: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7frame #15: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524frame #16: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482frame #17: 0x00007fff48533c32 UIKitCore`-[UITransitionView hitTest:withEvent:] + 44frame #18: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87frame #19: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121frame #20: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7frame #21: 0x00007fff23b848fc CoreFoundation&#

这篇关于iOS UIGestureRecognizer与原生响应机制处理流程分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/936492

相关文章

SpringBoot3.X 整合 MinIO 存储原生方案

《SpringBoot3.X整合MinIO存储原生方案》本文详细介绍了SpringBoot3.X整合MinIO的原生方案,从环境搭建到核心功能实现,涵盖了文件上传、下载、删除等常用操作,并补充了... 目录SpringBoot3.X整合MinIO存储原生方案:从环境搭建到实战开发一、前言:为什么选择MinI

SpringBoot结合Docker进行容器化处理指南

《SpringBoot结合Docker进行容器化处理指南》在当今快速发展的软件工程领域,SpringBoot和Docker已经成为现代Java开发者的必备工具,本文将深入讲解如何将一个SpringBo... 目录前言一、为什么选择 Spring Bootjavascript + docker1. 快速部署与

MySQL中的LENGTH()函数用法详解与实例分析

《MySQL中的LENGTH()函数用法详解与实例分析》MySQLLENGTH()函数用于计算字符串的字节长度,区别于CHAR_LENGTH()的字符长度,适用于多字节字符集(如UTF-8)的数据验证... 目录1. LENGTH()函数的基本语法2. LENGTH()函数的返回值2.1 示例1:计算字符串

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

Android ClassLoader加载机制详解

《AndroidClassLoader加载机制详解》Android的ClassLoader负责加载.dex文件,基于双亲委派模型,支持热修复和插件化,需注意类冲突、内存泄漏和兼容性问题,本文给大家介... 目录一、ClassLoader概述1.1 类加载的基本概念1.2 android与Java Class

Python使用vllm处理多模态数据的预处理技巧

《Python使用vllm处理多模态数据的预处理技巧》本文深入探讨了在Python环境下使用vLLM处理多模态数据的预处理技巧,我们将从基础概念出发,详细讲解文本、图像、音频等多模态数据的预处理方法,... 目录1. 背景介绍1.1 目的和范围1.2 预期读者1.3 文档结构概述1.4 术语表1.4.1 核

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

MySQL中的锁机制详解之全局锁,表级锁,行级锁

《MySQL中的锁机制详解之全局锁,表级锁,行级锁》MySQL锁机制通过全局、表级、行级锁控制并发,保障数据一致性与隔离性,全局锁适用于全库备份,表级锁适合读多写少场景,行级锁(InnoDB)实现高并... 目录一、锁机制基础:从并发问题到锁分类1.1 并发访问的三大问题1.2 锁的核心作用1.3 锁粒度分