1. 前言
在介绍元素标识之前,先回顾一下之前的《可视化全埋点系列文章之性能介绍篇》,依据这篇文章咱们理解到:可视化全埋点事件,是通过可视化的形式,把某些全埋点事件创立成一个重新命名的虚构事件[1],进而从数量宏大的全埋点事件中疾速筛选到咱们所关怀的事件[2]。
那么问题来了,如何将 SDK 触发的元素点击事件 $AppClick 和前端定义的可视化全埋点事件进行匹配?也就是说,保留了哪些配置信息,就能够惟一标识一个元素,从而筛选出这个元素触发的 $AppClick 事件?
答案是:只有精确地进行元素标识,咱们能力将可视化全埋点创立的虚构事件和元素进行匹配,从而精确筛选出这个元素触发的 $AppClick 事件。
上面,咱们就来看下可视化全埋点中如何进行元素标识。
2. 什么是元素标识
2.1. 概念
所谓元素标识,也就是通过某些信息,能够惟一标识某个元素。依据概念,须要实现两点:
依据信息,可能筛选出所需的元素,不会脱漏;
依据信息,只能筛选出所需的元素,不会多查。
例如:依据某些要害信息,能够惟一定位 App 中商品详情页面的“退出购物车”这个按钮元素。须要保障既不会匹配成“立刻购买”按钮,也不会匹配成商品列表或其余页面的“退出购物车”按钮。如图 2-1 所示:
图 2-1 商品详情页面的“退出购物车”按钮
2.2. 常见问题
保障元素的惟一标识,意味着在各种简单环境,都须要保障元素的惟一匹配。在理论我的项目开发中,会面临一些常见问题:
2.2.1. 零碎兼容
采纳的元素标识计划,须要兼容不同的零碎。例如:iOS14 应用的标识信息,在其余零碎也能惟一标识这个元素。
2.2.2. 设施兼容
元素标识须要兼容不同的设施,设施差别对元素标识的次要影响是屏幕尺寸大小。在 iOS 上,大多数 App 开发都会采纳主动布局进行屏幕适配。也就是说,可能同一个元素在 iPhone5s 显示在屏幕最底部,然而在 iPhone11 Pro Max 就可能显示在屏幕两头。对于不同的设施,须要能够应用同一套标识信息进行惟一匹配。
2.2.3. 款式扭转
上文提到的惟一标识某个元素,是绝对于用户交互而言的。随着 App 版本升级,元素的显示款式可能会发生变化,包含但不限于:
把按钮的色彩从“红色”改成“蓝色”;
把文字内容从“退出购物车”改成“增加购物车”;
把按钮形态从圆角改成矩形;
挪动按钮的地位。
尽管显示款式产生了变动,然而对于以后 App 的用户而言,点击的都还是“增加商品到购物车”这个按钮。至于形态或色彩,用户并不关注。
用户行为采集是为了撑持业务剖析,从业务的角度来看,因为这些款式变动都没有扭转这个元素的业务性能(例如把一个商品增加到购物车),所以也不影响最终的剖析后果。
因而,即便元素的款式变动了,之前存储的标识信息,还能够持续标识这个元素。
3. 如何进行元素标识
理解元素标识的概念和常见问题之后,咱们来看下进行元素标识的计划有哪些。
3.1. Target + Action + Tag
3.1.1. 计划阐明
在 iOS 开发中,针对一个元素(例如 UIButton)增加点击事件,个别的实现办法如下:
MyViewController.m
- (void)viewDidLoad {[super viewDidLoad];
// 省略其余逻辑
[self.myButton addTarget:self action:@selector(onButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (IBAction)onButtonClick:(UIButton *)sender {// 按钮点击的业务实现}
针对 UIButton,只有存在 target-action,才有可能触发点击事件。
一般来说,某个元素增加 target-action 创立点击事件后,这时候 target-action 是惟一确定的。另一方面,同一个页面(即 target 雷同)不同元素增加点击事件对应的办法名 action 个别是不同的。
当然,如果多个不同按钮增加同一个 action,在业务开发中通常会给按钮设置 tag,并在 action 中应用 tag 辨别不同元素。例如:
- (IBAction)onButtonClick:(UIButton *)sender {if (sender.tag == 1) {// 按钮 1 点击} else if (sender.tag == 2) {// 按钮 2 点击} else {// 其余按钮点击解决}
}
因而,通过 target + action + tag 能够标识一个按钮元素。针对一般按钮点击,能够通过 hook UIApplication 的 – sendAction:to:from:forEvent: 办法采集按钮的点击事件[3]。而后在 hook 后的办法中,获取 target + action + tag 拼接,即可获取元素标识。实现如下:
- (BOOL)sa_sendAction:(SEL)action to:(id)to from:(id)from forEvent:(UIEvent *)event {UIView *element = (UIView *)from;
// 解析元素标识
NSString *identifier = [NSString stringWithFormat:@"%@/%@/%ld", [to class], NSStringFromSelector(action), element.tag];
// 获取其余信息,触发埋点事件
return [self sa_sendAction:action to:to from:from forEvent:event];
}
最终获取到元素标识模式如下:
“MyViewController/onButtonClick:/0”
3.1.2. 优缺点
长处:
计划实现比较简单;
能够兼容基于 target-action 增加点击事件的元素,解决 UIControl 及其子类,手势点击事件的元素标识也实用。
毛病:
不适用于 UITableViewCell 这种基于 delegate 并通过代理办法 – tableView:didSelectRowAtIndexPath: 采集点击事件的元素,相似的还有 UICollectionViewCell、UIAlertView 等;
如果不同元素增加雷同 action,并且未设置 tag(action 外部实现可能通过文字内容或其余形式辨别),这种场景应用 target + action + tag 拼接的元素标识无奈辨别;
因为依赖于 action 办法名,如果在前期 App 版本迭代过程中批改了 action 的名称,会导致元素标识被批改,这样和以前触发事件的历史数据就无奈兼容。
3.2. 响应者链全门路
3.2.1. 计划阐明
3.2.1.1. 响应者和响应者链
咱们晓得,UIResponder 是 UIKit 框架中响应用户操作的基类,在 UIResponder 类中定义了专门用于解决用户交互事件的接口。咱们熟知的 UIView、UIViewController、UIApplication、UIWindow 都是间接或间接继承自 UIResponder 的子类,因而它们的实例都能够响应用户交互行为,从而形成了响应者。在用户操作 App 过程中,由离用户最近的 view 向零碎层层传递,从而形成了响应者链,例如:interactive view –> superview(nextResponder) –> ….. –> viewController –> window –> Application –> AppDelegate,如图 3-1 所示:
图 3-1 响应者链(来源于 Apple 开发者文档)[4]
3.2.1.2. 拼接响应者链
依据响应者链的定义咱们能够看出,任意一个可点击的 view,点击过程中的响应者链是一条惟一并确定的链路。如果将这个链路上的所有响应者类名进行拼接形成字符串,这串字符就造成了以后 view 的惟一特色,从而有了元素标识。
另一方面,同一个 App 不同元素对应响应者链上的 window(个别是 keyWindow)、Application 和 AppDelegate 这三者都是雷同的,也就没有辨识作用,因而在拼接元素标识过程中不用蕴含。
拼接响应者链的大抵实现逻辑如下:
SAVisualizedUtils.m
// 递归遍历响应者链获取元素门路
+ (NSArray<NSString *> *)viewPathsForResponder:(UIResponder *)responder {NSMutableArray *viewPathArray = [NSMutableArray array];
do {
// 遍历 view 层级门路
NSString *className = NSStringFromClass(responder.class);
[viewPathArray addObject:className];
} while ((responder = (id)responder.nextResponder) && [responder isKindOfClass:UIView.class] && ![responder isKindOfClass:UIWindow.class]);
if ([responder isKindOfClass:UIViewController.class]) {
// 遍历 controller 层门路
[viewPathArray addObjectsFromArray:[self viewPathsForViewController:(UIViewController *)responder]];
}
return viewPathArray;
}
// 拼接元素门路,获取以后元素标识
+ (NSString *)identifierForResponder:(UIResponder *)responder {NSArray *viewPaths = [[[self viewPathsForView:responder] reverseObjectEnumerator] allObjects];
NSString *identifier = [viewPaths componentsJoinedByString:@"/"];
return viewPath;
}
这样针对一个自定义的 CustomButton,最终构建的元素标识如下:
“UINavigationController/AutoTrackViewController/UIView/CustomButton”
3.2.1.3. 同类元素辨别
测试发现,一个页面中如果存在多个雷同类型的元素,上述标识办法无奈惟一标识某个元素。为了辨别这些同类元素,能够引入此元素在父视图 subviews 中的序号 index。
这样以来,如果一个页面蕴含多个元素,它们构建的元素标识构造如下:
"UINavigationController/AutoTrackViewController/UIView/CustomButton[0]"
"UINavigationController/AutoTrackViewController/UIView/CustomButton[1]"
"UINavigationController/AutoTrackViewController/UIView/UILabel[2]"
3.2.1.4. 优化
在理论开发过程中,对于一些 UI 交互比拟灵便的页面,为了满足性能须要,开发人员可能会调用 – insertSubview:atIndex:、- exchangeSubviewAtIndex:withSubviewAtIndex:、- insertSubview:aboveSubview: 等办法。这样会影响元素所在父视图的 subviews 中索引值,从而影响最终的元素标识。例如:上述页面中的按钮挪动到页面的最上层,这样最终拼接的元素标识构造如下:
"UINavigationController/AutoTrackViewController/UIView/CustomButton[0]"
"UINavigationController/AutoTrackViewController/UIView/UILabel[1]"
"UINavigationController/AutoTrackViewController/UIView/CustomButton[2]"
能够发现,第二个 CustomButton 和 UILabel 的元素标识都扭转了,这样元素点击事件 $AppClick 中采集的对应属性也会变动,导致之前定义的可视化全埋点事件无奈精确查问。
通过剖析可知,不同类型的元素其实没必要做 index 辨别。因而在获取 index 的时候,不能简略地获取以后元素在 subviews 中的序号,还须要做同类元素的归并解决。大抵实现如下:
// 获取元素序号
+ (NSInteger)itemIndexForResponder:(UIResponder *)responder {
NSArray<UIResponder *> *brothersResponder;
if ([responder isKindOfClass:UIView.class]) {UIResponder *next = [responder nextResponder];
if ([next isKindOfClass:UIView.class]) {brothersResponder = [(UIView *)next subviews];
}
} else if ([responder isKindOfClass:UIViewController.class]) {brothersResponder = [(UIViewController *)responder parentViewController].childViewControllers;
}
NSString *className = NSStringFromClass(responder.class);
NSInteger count = 0;
NSInteger index = -1;
for (UIResponder *res in brothersResponder) {if ([className isEqualToString:NSStringFromClass(res.class)]) {count++;}
if (res == responder) {index = count - 1;}
}
// 单个 UIViewController(即不存在其余兄弟 viewController)拼接门路,不须要序号
if ([responder isKindOfClass:UIViewController.class] && count == 1) {return -1;}
/* 序号阐明
-1:nextResponder 不是父视图或同类元素,比方 controller.view,波及门路不带序号
>=0:元素序号
*/
return count == 0 ? -1 : index;
优化之后,上述页面元素最终拼接的元素标识构造如下:
"UINavigationController/AutoTrackViewController/UIView/CustomButton[0]"
"UINavigationController/AutoTrackViewController/UIView/UILabel[0]"
"UINavigationController/AutoTrackViewController/UIView/CustomButton[1]"
如果是 UITabBarController 嵌套 UINavigationController,元素所在 viewController 为 UINavigationController 的 topViewController,则拼接元素标识为:
"UITabBarController/UINavigationController/AutoTrackViewController/UIView/CustomButton[0]"
"UITabBarController/UINavigationController/AutoTrackViewController/UIView/UILabel[0]"
"UITabBarController/UINavigationController/AutoTrackViewController/UIView/CustomButton[1]"
3.2.2. 优缺点
长处:
绝对精准地标识一个元素,不受 action 办法名或文字内容等因素影响;
兼容 target-action、UITableViewCell 等各种形式实现点击事件采集的元素标识,能够满足所有点击事件的元素标识。
毛病:
通过不同的形式跳转进入同一个页面,页面内元素的标识可能不同。例如:上述示例中是否通过 UITabBarController 嵌套,会影响页面内元素的标识。因而,针对简单的页面,难以用同一套元素标识进行匹配;
针对 UITableViewCell,波及 Cell 重用后,同一个 Cell 在列表中行数可能会变动。间接应用 index 可能无奈精确匹配列表中某一行的元素,更无奈反对针对列表元素进行不限元素地位标识。
3.3. ViewPath + ScreenName + Position / Content
3.3.1. 计划阐明
3.3.1.1. 概念
这个计划是在拼接响应者链全门路的根底上,针对其局限性进行革新而成的一种计划,也是咱们目前正在应用的计划。先解释一下各项的含意:
ViewPath
元素门路,依据元素响应者链拼接而成的字符串,以以后元素所在 viewController 的 view 作为终点。例如:页面中局部元素的 viewPath 示例如下:
一般按钮:"UIView/CustomButton[0]"
UILabel:"UIView/UILabel[0]"
UIScrollView 嵌套 按钮:"UIView/UIScrollView[0]/UIButton[0]"
导航栏 UIBarButtonItem:"UILayoutContainerView/UINavigationBar[0]/_UINavigationBarContentView[0]/_UIButtonBarStackView[0]/_UIButtonBarButton[0]"
UITableViewCell:"UIView/UITableView[0]/UITableViewCell[0][-]" // [-] 为通配符,能够匹配不同地位元素
ScreenName
即页面名称,也就是元素所在以后页面 viewController 的类名。
Position
示意元素地位,只有列表类型元素才反对,用于反对是否限定元素地位。UITableViewCell 的 position 构造即 “indexPath.section : indexPath.row”。
UISegmentedControl 点击的 position 间接应用 selectedSegmentIndex。
Content
元素内容,用于反对是否限定元素内容,即以后元素显示的文字内容。
3.3.1.2. 筛选标识
依据 viewPath、screenName、position、content 几个维度的特色,在元素标识过程中灵便地组合应用,从而满足简单的利用场景和不同的元素类型:
针对一般元素,应用 viewPath + screenName 两个维度的特色,即可进行惟一标识。例如:某个按钮的筛选条件为:viewPath == “UIView/CustomButton[0]” 且 screenName == “AutoTrackViewController”;
如果须要限度元素内容来标识元素,即在定义可视化全埋点事件的时候,须要限定元素内容,减少筛选 content 条件即可。例如:content == “ 退出购物车 ”;
UITableViewCell、UICollectionViewCell 等列表元素。通过是否蕴含元素地位条件,即可匹配某一行列表或整个列表元素,从而反对限定元素地位和不限定元素地位两种形式定义可视化全埋点事件。
3.3.2. 优缺点
长处
最大的长处是应用灵便:将每个维度的特色尽可能简单化,而后通过不同维度的条件组合,能够满足不同的利用场景和元素类型,从而反对限定元素地位、限定元素内容等性能;
兼容简单的页面组合形式:根本匹配条件是 viewPath + screenName,同一个页面(例如商品详情)应用不同的页面嵌套(例如是否蕴含 UITabBarController 或 UINavigationController 嵌套)或不同的跳转形式(push 或 present),都不会影响页面内元素标识。
毛病
存在零碎兼容性问题:此计划也应用了元素类名,并且响应者链拼接最终会依赖页面层级。在 iOS 零碎的降级过程中,同一种性能,零碎实现的用户交互的元素类名和层级可能不同。例如:iOS11 前后 UIBarButtonItem 外部实现不同,从而获取到的 viewPath 不同:
iOS13: UILayoutContainerView/UINavigationBar[0]/_UINavigationBarContentView[0]/_UIButtonBarStackView[0]/_UIButtonBarButton[0]
iOS10: UILayoutContainerView/UINavigationBar[0]/UINavigationButton[0]
计划依赖于页面名称 screenName,然而针对 tabbar 控件,因为非选中状态 UITabBarItem 所在的页面可能尚未初始化,此时获取页面名称为以后 App 显示的页面。等到点击 tab 切换页面后,页面名称会扭转。因而针对 UITabBarItem 临时只反对 selectedItem 状态的元素进行标识;
如果 App 后续迭代中批改了页面层级或在 subviews 中的序号 index,可能导致 viewPath 批改,最终影响元素标识。
4. 总结
本文是可视化全埋点系列文章的第二篇,开始介绍可视化全埋点的实现,这里次要介绍了可视化全埋点实现的关键步骤 — 元素标识。
首先阐明了元素标识的起因,而后介绍了元素标识计划的演进。
从文中咱们能够晓得,通过艰巨地摸索和继续的迭代后,目前咱们选用的计划,依然存在零碎兼容和非凡场景的反对问题。因而,这只是在满足业务需要和攻破技术瓶颈衡量下的最优抉择,而不是最终计划。后续咱们依然会继续摸索,解决遗留的问题。
这个过程,就像爬山,不停地攀登。山顶可能没有新雪,只有前人的足迹,甚至没有峰顶。然而你达到了从未达到的高度,看到了全新的风光,而身后脚步记录的,便是咱们的生命。
最初,欢送大家退出开源社区一起探讨,
5. 参考文献
[1] https://manual.sensorsdata.cn… 虚构事件 -22253779.html
[2] 可视化全埋点系列文章之性能介绍篇
[3] https://github.com/sensorsdat…
[4] https://docs-assets.developer…
文章来自公众号——神策技术社区