一、前言
从之前的可视化全埋点系列文章之功能介绍篇可以了解到,可视化全埋点主要功能就是定义 App 上的页面浏览和元素点击事件。因为可以在前端选中 App 上的元素,并对 App 上的元素点击事件重新定义虚拟事件[1],所以前端是需要获取到 App 上的所有元素,主要包括:
1. 元素位置,即元素相对于手机屏幕的坐标
2. 元素尺寸,元素的尺寸大小,用于前端渲染
3. 元素信息,包括元素标识、元素位置和内容等,用于定义事件
这其实就是可视化全埋点为什么需要元素遍历的原因。
二、什么是元素遍历
2.1 概念
介绍元素遍历之前,先说明一下本文中的元素。可视化全埋点用于可点击元素的圈选,只需要关注用户交互元素。因此这里的元素指的是 UI 元素,表示我们在 App 里可以看见的任何元素。有些元素用于响应交互,比如 UIButton、UITableView、UISwitch、UISegmentedControl 等。有些元素用于提供丰富的内容,比如 UIImageView、UILabel、Web 容器(WebView)等。
元素遍历,顾名思义,就是查询遍历 App 内的所有元素。虽然最终我们需要圈选埋点的只是可点击元素,但是因为需要递归去找到所有可点击元素,并且可点击元素可能是普通元素的子元素,所以在遍历查询过程中,不可避免地需要找到其他非点击元素,比如 UIView、UIViewController、UIWindow 等。
2.2 注意事项
在元素遍历过程中,有几点需要注意:
-
不重:即同一个元素,不能被重复找到,避免数据冗余,优化上传性能。比如针对不同类型的元素,可能需要采取不同的查找策略,保证同一个元素最终只会被找到一次
-
不漏:不能遗漏可交互元素,否则可能导致该元素无法圈选,从而无法定义可视化全埋点事件
-
不多:需要忽略 App 中无法交互的元素,比如设置隐藏、透明或超出屏幕的元素。如果找到了不可见的元素并在前端渲染,可能影响正常元素的圈
三、如何进行元素遍历
元素遍历,并使用某个可点击元素定义可视化全埋点事件,需要解决的几个关键问题总结如下:
1. 首先需要找到元素,也就是从运行中的 App 中遍历找到所有元素
2. 获取状态,即标记这个元素是否可以定义事件
3. 元素位置,计算元素位置,从而在前端进行渲染
4. 优先级,即如果两个元素坐标重叠,前端鼠标 hover 后,应该优先响应哪个元素
下面介绍如何解决上述问题。
3.1 可见性
在页面层级中,有很多元素是不可见的,比如被隐藏或者超出屏幕。这些元素不可交互,在元素遍历过程中需要进行移除。
元素不可见的常见原因如下:
1. 设置隐藏
2. 设置透明度小于 0.01(iOS 中,透明度小于 0.01 即不可见)
3. 尺寸为 0 或超出屏幕
4. 从父视图移出
针对上述原因导致的元素不可见,需要进行屏蔽,实现如下:
// 判断一个 view 是否显示
- (BOOL)sensorsdata_isVisible {
if (!(self.window && self.superview)) {
return NO;
}
if(self.alpha <= 0.01 || self.isHidden) {
return NO;
}
// 计算 view 在 keyWindow 上的坐标
CGRect rect = [self convertRect:self.bounds toView:nil];
// 若 size 为 CGSizeZero
if (CGRectIsNull(rect) || CGSizeEqualToSize(rect.size, CGSizeZero)) {
return NO;
}
}
对于被覆盖元素,虽然不可见,但是在 iOS 中,如果覆盖的元素设置不可交互,即 userInteractionEnabled = NO,那么被覆盖的元素仍然可交互(感兴趣的读者可以自行验证一下)。所以被覆盖的元素,暂时不屏蔽。
3.2 元素遍历
元素遍历,就是解决找到元素的问题。关于 iOS 开发中元素遍历,常用的有两种方案,下面我们分别进行探讨。
3.2.1. 方案 1:遍历所有 subviews
3.2.1.1. 方案说明
获取当前 window,一般情况下,直接获取 keyWindow 即可。然后从当前 keyWindow,递归遍历所有子元素,直到没有子元素为止。方案实现相对简单,此处不再展开说明。
3.2.1.2. 优缺点
优点:
1. 维护成本小:从代码实现可以看出,这种方案逻辑简单清晰、方便维护
2. 兼容问题少:直接递归遍历所有 subviews,不需要针对不同元素类型做兼容处理,相对比较稳定
缺点:
1. 存在很多冗余元素:直接递归遍历,会存在大量私有类型或其他冗余元素,比如从 navigationController.view 找到 viewController.view 之间,会经过 UINavigationTransitionView 和 UIViewControllerWrapperView 两个私有类型的 view。同时 UITextField、UIButton 等是通过其他多层子元素组合而成,这些元素也不直接涉及用户交互,属于冗余元素。我们最终需要的,只是 viewController.view 上的各种可交互控件,比如 UIButton、UITableViewCell 等
2. 查找路径比较长:比如从 navigationController.view 找到 viewController.view,或者从 keyWindow 找到 navigationController.view 之前,都存在多层私有 view,导致查找路径比较长,最终影响查询效率和性能。如图 3-1 所示ÿ