當(dāng)已經(jīng)確定了如何通過 AOP 在業(yè)務(wù)中插入埋點代碼后,即可開始采集埋點數(shù)據(jù),然后進(jìn)行上報。
構(gòu)建的埋點數(shù)據(jù)可以分為兩部分:
- 構(gòu)建一個 Key-Value 數(shù)據(jù)結(jié)構(gòu)存放此次埋點的數(shù)據(jù)
- 構(gòu)建一個唯一 ID 用于標(biāo)識事件,并使用
event_code
作為 key 存放步驟 1 中的數(shù)據(jù)中
本文主要描述如何生成第二點中的唯一 ID
在下文中,event code 就是事件唯一 ID
要求
用戶操作事件埋點,一般用于分析用戶行為、用戶習(xí)慣、某個按鈕的日點擊量或者時間段點擊量等等。為了更便捷的分析這些數(shù)據(jù),就會對事件 ID 有一定的要求。
在每次無痕埋點數(shù)據(jù)采集過程中,都會取到一大堆亂七八糟的數(shù)據(jù),而為了準(zhǔn)確標(biāo)識某個用戶操作事件,我們必須要有統(tǒng)一的事件唯一 ID 生成方案,而這個方案必須滿足以下條件:
- 同一個界面,同一個按鈕,使用一個 ID
不因為當(dāng)前不同業(yè)務(wù)數(shù)據(jù)環(huán)境導(dǎo)致 ID 變化,這樣有助于大數(shù)據(jù)分析。
如:使用按鈕標(biāo)題拼接唯一 ID(比如某個按鈕標(biāo)題是當(dāng)前位置到某個位置的距離,這個距離會根據(jù)用戶實際位置的變化而變化),這會導(dǎo)致同一個功能,不同的標(biāo)題產(chǎn)生多個 ID,并且對應(yīng)同一個事件。
- 不同界面,或者同一界面的按鈕,不能使用相同的 ID
如:使用按鈕類名拼接唯一 ID,如果按鈕被復(fù)用,就有可能導(dǎo)致兩個事件的 ID 湊巧相同。
總之,我們要做到事件和 ID 是一一對應(yīng)的關(guān)系,而不是一對多,也不是多對一。
現(xiàn)在,基于上述條件生成唯一 ID。
生成 delegate 埋點的唯一 ID
delegate 埋點一般為下面兩種:
-[UITableView tableView:didSelectRowAtIndex:]
-[UICollectionViewDelegate collectionView:didSelectItemAtIndexPath:]
我們 hook 了
-[UITableView setDelegate:]
方法,創(chuàng)建了一個Proxy
對象作為中間對象,偽裝了實際的 delegate,并攔截了對應(yīng)的點擊回調(diào)方法。所以我們采集的數(shù)據(jù)可以在-[UITableView setDelegate:]
中獲取初始數(shù)據(jù),以及在-[Proxy tableView:didSelectRowAtIndex:]
中采集實際點擊數(shù)據(jù)。
采集數(shù)據(jù)
1. setDelegate: 采集初始化數(shù)據(jù)
在設(shè)置 delegate 時,我們可以拿到 UITableView
的類名(如果被繼承了的話),已經(jīng)業(yè)務(wù)實際的 delegate
對象。由于這兩個數(shù)據(jù)在 -[Proxy tableView:didSelectRowAtIndex:]
方法中也能拿到,所以此處不會記錄這兩個數(shù)據(jù)
2. tableView:didSelectRowAtIndex: 采集實際點擊數(shù)據(jù)
當(dāng)用戶實際點擊某一個 Cell 時,會觸發(fā)此方法。我們可以在此方法中獲取非常豐富的數(shù)據(jù):
- self(Proxy 對象)
- 參數(shù) tableView
- tableView 可以使用 UIResponse 獲取對應(yīng)的 ViewController
- 參數(shù) indexPath
- tableView + indexPath 可以獲取對應(yīng)點擊的 Cell 對象
- self.delegate(業(yè)務(wù)實際的 delegate)
- ...
所以在此方法中我們可以至少拿到 6 個數(shù)據(jù),接下來進(jìn)行分析,使用這 6 個數(shù)據(jù)拼接事件 ID。
拼接事件
首先明確,當(dāng)我們拿到某個對象時,代表我們可以拿到兩個數(shù)據(jù):1. 該對象的地址,2. 該對象的類名。由于地址的隨機(jī)性很大,為了保證上文中的條件,所以不會使用該對象的地址來拼接事件。
Proxy 對象
Proxy 對象是由埋點 SDK 生成的,所以類名一成不變,故 Proxy 對象不能拿來拼接事件 ID。
TableView 對象
TableView 大部分是 UITableView
,由于基本不會去繼承他,所以不會使用 TableView 的類名。
ViewController 對象
ViewController 一般為自定義的,所以類名也是根據(jù)業(yè)務(wù)實際情況來定,故 ViewController 的類名可以作為唯一 ID 的一部分。
IndexPath 對象
NSIndexPath 是標(biāo)識行數(shù),由于 TableView 行數(shù)可變,不確定。故如果使用 IndexPath 里的數(shù)據(jù)拼接 ID,將會產(chǎn)生大量不同的名字。所以 IndexPath 對象不能用。但是為了準(zhǔn)確標(biāo)識用戶點擊了哪一行的 cell,可以將 IndexPath 當(dāng)做別的參數(shù)來上報。
Cell 對象
大部分的 Cell 都是自定義的,所以類名也是根據(jù)視圖樣式來定,故 Cell 的類名可以作為唯一 ID 的一部分。
綜述,我們可以拼接 ViewController 和 Cell 來拼接 ID。但是如果一個 VC 中出現(xiàn)了兩個 TableView(如外賣 app 的菜單頁面),或者近似的兩個 TableView。故再加一個 TableView.delegate.className。
最終事件 ID 如下:
VCClassName
#DelegateClassName
#CellClassName
@implementation MyTableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// 轉(zhuǎn)發(fā)給業(yè)務(wù)
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 的回調(diào),一般使用如下代碼
-[UIControl addTarget:action:events:]
-[UIGestureRecognizer initWithTarget:action:]
我們 hook 了
-[UIControl addTarget:action:events:]
方法,創(chuàng)建了一個Action
對象作為附屬對象,和實際的 target 一同添加到 UIControl 中。當(dāng) UIControl 觸發(fā)了事件,就會同時向業(yè)務(wù)對象和Action
對象發(fā)送消息,從而產(chǎn)生埋點。故我們可以在-[UIControl addTarget:action:events:]
方法中獲取到 target、action、event。還能從-[Action action:]
方法中獲取實時埋點數(shù)據(jù)。
采集
-[UIControl addTarget:action:events:]
在此方法中,我們可以獲取到 UIControl 類名,target 對象,action 方法名,events 事件名。由于后三個數(shù)據(jù)在下面的方法中無法獲取,所以會記錄后三個數(shù)據(jù)到 Action 對象中,供 Action 對象在觸發(fā)下面的方法時獲取對應(yīng)數(shù)據(jù):
// 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:]
此方法是實際埋點執(zhí)行的方法,由于此方法只能獲取 self(Action 對象)和 sender(UIControl 對象),故實際埋點數(shù)據(jù)還是依賴前一個方法臨時保存的數(shù)據(jù)。
此時我們可以在方法中構(gòu)成埋點數(shù)據(jù)。
- self(Action 對象)
- sender(UIControl 對象)
- VC(可以根據(jù) UIControl 獲取所在 VC)
- self.targetName(target 類名)
- self.action(action 方法名)
- self.events(events 值)
拼接事件
Action 對象
此對象是 SDK 內(nèi)部對象,無任何信息
sender
控件對象,大部分按鈕不會繼承,所以也不會有信息。
self.targetName
響應(yīng)者類名,此類一般為 VC 的類名,或者某個 View 的類名,故此信息可用于拼接。
self.action
響應(yīng)方法名,于前一個相同,但不同事件一般會有不同方法回調(diào),所以方法名也可以作為唯一事件 ID。
self.events
事件類型,不同控件不同事件,但按鈕基本為 UIControlEventsTouchUpInside
,如果要區(qū)分不同控件則可以加入,本文只考慮按鈕情況。故不加入此信息
最終事件 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:
總結(jié)
我們盡可能采集了事件數(shù)據(jù),拼接成了事件唯一 ID。事件唯一 ID 的拼接可以根據(jù)實際的埋點需求來定,并非一成不變。
以上就是 iOS 端無痕埋點解決方案事件 ID 部分的實現(xiàn)。
在接下來的篇幅中,我將介紹如何在埋點中攜帶 UI 控件上獲取不到的業(yè)務(wù)數(shù)據(jù)。