当已经确定了如何通过 AOP 在业务中插入埋点代码后,即可开始采集埋点数据,然后进行上报。
构建的埋点数据可以分为两部分:
- 构建一个 Key-Value 数据结构存放此次埋点的数据
- 构建一个唯一 ID 用于标识事件,并使用
event_code
作为 key 存放步骤 1 中的数据中
本文主要描述如何生成第二点中的唯一 ID
在下文中,event code 就是事件唯一 ID
要求
用户操作事件埋点,一般用于分析用户行为、用户习惯、某个按钮的日点击量或者时间段点击量等等。为了更便捷的分析这些数据,就会对事件 ID 有一定的要求。
在每次无痕埋点数据采集过程中,都会取到一大堆乱七八糟的数据,而为了准确标识某个用户操作事件,我们必须要有统一的事件唯一 ID 生成方案,而这个方案必须满足以下条件:
- 同一个界面,同一个按钮,使用一个 ID
不因为当前不同业务数据环境导致 ID 变化,这样有助于大数据分析。
如:使用按钮标题拼接唯一 ID(比如某个按钮标题是当前位置到某个位置的距离,这个距离会根据用户实际位置的变化而变化),这会导致同一个功能,不同的标题产生多个 ID,并且对应同一个事件。
- 不同界面,或者同一界面的按钮,不能使用相同的 ID
如:使用按钮类名拼接唯一 ID,如果按钮被复用,就有可能导致两个事件的 ID 凑巧相同。
总之,我们要做到事件和 ID 是一一对应的关系,而不是一对多,也不是多对一。
现在,基于上述条件生成唯一 ID。
生成 delegate 埋点的唯一 ID
delegate 埋点一般为下面两种:
-[UITableView tableView:didSelectRowAtIndex:]
-[UICollectionViewDelegate collectionView:didSelectItemAtIndexPath:]
我们 hook 了
-[UITableView setDelegate:]
方法,创建了一个Proxy
对象作为中间对象,伪装了实际的 delegate,并拦截了对应的点击回调方法。所以我们采集的数据可以在-[UITableView setDelegate:]
中获取初始数据,以及在-[Proxy tableView:didSelectRowAtIndex:]
中采集实际点击数据。
采集数据
1. setDelegate: 采集初始化数据
在设置 delegate 时,我们可以拿到 UITableView
的类名(如果被继承了的话),已经业务实际的 delegate
对象。由于这两个数据在 -[Proxy tableView:didSelectRowAtIndex:]
方法中也能拿到,所以此处不会记录这两个数据
2. tableView:didSelectRowAtIndex: 采集实际点击数据
当用户实际点击某一个 Cell 时,会触发此方法。我们可以在此方法中获取非常丰富的数据:
- self(Proxy 对象)
- 参数 tableView
- tableView 可以使用 UIResponse 获取对应的 ViewController
- 参数 indexPath
- tableView + indexPath 可以获取对应点击的 Cell 对象
- self.delegate(业务实际的 delegate)
- ...
所以在此方法中我们可以至少拿到 6 个数据,接下来进行分析,使用这 6 个数据拼接事件 ID。
拼接事件
首先明确,当我们拿到某个对象时,代表我们可以拿到两个数据:1. 该对象的地址,2. 该对象的类名。由于地址的随机性很大,为了保证上文中的条件,所以不会使用该对象的地址来拼接事件。
Proxy 对象
Proxy 对象是由埋点 SDK 生成的,所以类名一成不变,故 Proxy 对象不能拿来拼接事件 ID。
TableView 对象
TableView 大部分是 UITableView
,由于基本不会去继承他,所以不会使用 TableView 的类名。
ViewController 对象
ViewController 一般为自定义的,所以类名也是根据业务实际情况来定,故 ViewController 的类名可以作为唯一 ID 的一部分。
IndexPath 对象
NSIndexPath 是标识行数,由于 TableView 行数可变,不确定。故如果使用 IndexPath 里的数据拼接 ID,将会产生大量不同的名字。所以 IndexPath 对象不能用。但是为了准确标识用户点击了哪一行的 cell,可以将 IndexPath 当做别的参数来上报。
Cell 对象
大部分的 Cell 都是自定义的,所以类名也是根据视图样式来定,故 Cell 的类名可以作为唯一 ID 的一部分。
综述,我们可以拼接 ViewController 和 Cell 来拼接 ID。但是如果一个 VC 中出现了两个 TableView(如外卖 app 的菜单页面),或者近似的两个 TableView。故再加一个 TableView.delegate.className。
最终事件 ID 如下:
VCClassName
#DelegateClassName
#CellClassName
@implementation MyTableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// 转发给业务
if ([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
[self.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
}
//埋点
NSString *event_code = ({
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
NSString *viewController = ({
UIResponder *responder = tableView;
while (responder) {
responder = responder.nextResponder;
if ([responder isKindOfClass:[UIViewController class]]) {
break;
} else if ([responder isKindeOfClass:[UIWindow class]]) {
break;
}
}
NSStringFromClass([responder class]);
});
NSString *targetName = NSStringFromClass([self.delegate class]);
NSString *cellName = NSStringFromClass([cell class]);
[NSString stringWithFormat:@"%@#%@#%@", viewController, targetName, cellName];
});
[Tracker trackEvent:event_code];
}
@end
举例:
UIViewController#UIViewController#UITableViewCell
UIViewController#MenuView#MenuCell
生成 Target-Action 埋点的唯一 ID
Target-Action 是手势和 UIControl 的回调,一般使用如下代码
-[UIControl addTarget:action:events:]
-[UIGestureRecognizer initWithTarget:action:]
我们 hook 了
-[UIControl addTarget:action:events:]
方法,创建了一个Action
对象作为附属对象,和实际的 target 一同添加到 UIControl 中。当 UIControl 触发了事件,就会同时向业务对象和Action
对象发送消息,从而产生埋点。故我们可以在-[UIControl addTarget:action:events:]
方法中获取到 target、action、event。还能从-[Action action:]
方法中获取实时埋点数据。
采集
-[UIControl addTarget:action:events:]
在此方法中,我们可以获取到 UIControl 类名,target 对象,action 方法名,events 事件名。由于后三个数据在下面的方法中无法获取,所以会记录后三个数据到 Action 对象中,供 Action 对象在触发下面的方法时获取对应数据:
// In UIControl+Hook.m
- (void)hook_addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events {
// Call origin method
[self hook_addTarget:target action:action forControlEvents:events];
// Create Action object
MyTargetAction *action = [[MyTargetAction alloc] init];
action.targetName = NSStringFromClass([target class]);
action.action = NSStringFromSelector(action);
action.events = events;
// Add Action object
[self hook_addTarget:action action:@selector(action:) forControlEvents:events];
}
-[Action action:]
此方法是实际埋点执行的方法,由于此方法只能获取 self(Action 对象)和 sender(UIControl 对象),故实际埋点数据还是依赖前一个方法临时保存的数据。
此时我们可以在方法中构成埋点数据。
- self(Action 对象)
- sender(UIControl 对象)
- VC(可以根据 UIControl 获取所在 VC)
- self.targetName(target 类名)
- self.action(action 方法名)
- self.events(events 值)
拼接事件
Action 对象
此对象是 SDK 内部对象,无任何信息
sender
控件对象,大部分按钮不会继承,所以也不会有信息。
self.targetName
响应者类名,此类一般为 VC 的类名,或者某个 View 的类名,故此信息可用于拼接。
self.action
响应方法名,于前一个相同,但不同事件一般会有不同方法回调,所以方法名也可以作为唯一事件 ID。
self.events
事件类型,不同控件不同事件,但按钮基本为 UIControlEventsTouchUpInside
,如果要区分不同控件则可以加入,本文只考虑按钮情况。故不加入此信息
最终事件 ID 如下:
VCClassName
#TargetClassName
#ActionName
// In MyTargetAction.m
- (void)action:(UIControl *)sender {
NSString *event_code = ({
NSString *viewController = ({
UIResponder *responder = sender;
while (responder) {
responder = responder.nextResponder;
if ([responder isKindOfClass:[UIViewController class]]) {
break;
} else if ([responder isKindeOfClass:[UIWindow class]]) {
break;
}
}
NSStringFromClass([responder class]);
});
[NSString stringWithFormat:@"%@#%@#%@", viewController, self.targetName, self.actionName];
});
[Tracker trackEvent:event_code];
}
举例:
UIViewController#UIViewController#onClick:
UIViewController#MenuItemCell#onClickAdd:
总结
我们尽可能采集了事件数据,拼接成了事件唯一 ID。事件唯一 ID 的拼接可以根据实际的埋点需求来定,并非一成不变。
以上就是 iOS 端无痕埋点解决方案事件 ID 部分的实现。
在接下来的篇幅中,我将介绍如何在埋点中携带 UI 控件上获取不到的业务数据。
网友评论