當已經確定了如何通過 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 控件上獲取不到的業務數據。