iOS hitTest 遍历子视图时为什么要逆序遍历?

1,865 阅读3分钟

在准备 iOS 面试的时候,经常会复习到和 hitTest 相关的知识点。如果视图是 hitTestable 的,那么当前 view 会依次递归调用 view.subviews 的 hitTest 方法。而遍历 view.subviews 的顺序是逆序而不是正序,这一度使我非常疑惑。

但是最近在开发一个需求的过程中让我对逆序遍历的这个做法突然有了一定的理解。

需求

首先介绍下需求背景。视图层级如下图所示,SubviewA 和 SubviewB 是 sibling 关系,他们有着共同的父视图,即蓝色的SuperView。而 SubviewA 和 SubviewB 分别属于两个 Framework,用 FrA 和 FrB 代表。

产品要求在 FrA 中提供设置 SubviewA 是否能够响应用户点击的接口,且禁用后, SubviewB 也不能响应。(先别吐槽这个奇怪的需求啦,探讨技术为主 ^_^)

视图层级

分析和解决

如果是正常情况那很好办,把 SuperView 的 userInteractionEnabled 设置为 NO,就搞定了。但是问题就在于只能在 FrA 库中对 SubviewA 进行处理。

PlanA

当时我的第一想法是把 SubviewA 的 userInteractionEnabled 设置为 NO。然后仔细一想,这样设置虽然解决了 SubviewA 禁止响应的问题,但是 SubviewB 还是会响应。行不通。

PlanB

然后想到了 hitTest,只要让 hitTest 方法的返回结果为 nil 就可以达到需求要求的效果。确定了这个思路后就继续往下想。但是 SubviewA 如果 hitTest 返回了 nil,那么 for 循环轮到 SubviewB 的时候其实一样能够响应。依然行不通。

PlanC

又思考了一下,既然返回 nil 不行,那干脆让 SubviewA 在 hitTest 中返回一个占位的 dummyView,把 dummyView 的 frame 设置为 0 就好了。 dummyView

于是,在 SubviewA 的视图层级上的所有视图的 hitTest 的返回值都是 dummyView。而在 hitTest 遍历到 SubviewA 返回 dummyView 后,hitTest 压根就不会再继续遍历 SubviewB 了。而 dummyView 又确实对事件不会做出任何响应。需求宣告完成!

也正是因为 hitTest 逆序遍历的特性,导致先遍历到 SubviewA,然后才是 SubviewB。PlanC 才能够达到需求指定的效果。

对 hitTest 逆序遍历的思考

那么问题来了,Apple 为什么要设计成逆序遍历,为什么不用符合直觉的顺序遍历呢?难道 Apple 事先就知道会有开发者有类似上文所述的需求,未卜先知地在 hitTest 里采用逆序遍历?

答案是——效率

后添加的 view 一般来说都要在先添加的 view 的上方,而后添加的 view 处于 view.subviews 数组靠后的位置。因此,hitTest 在逆序遍历 subviews 的时候有更大几率提前找到合适的 subview,此时就可以退出递归,剩下的 subviews 的视图层级不需要继续遍历。从而达到节省时间、节省 CPU 资源的效果。