用戶端埋點大概分為三類:
- 程式碼埋點
- 可視化埋點
- 無埋點
這篇文章主要介紹了 無埋點 的想法和實踐過程。
1、程式碼埋點
程式碼埋點,即在需要埋點的節點呼叫介面直接上傳埋點資料,第三方資料統計服務商也大都提供了程式碼埋點的 api,非常方便。
但是帶來一個問題,埋點程式碼散落在業務的各個地方,和業務耦合嚴重,尤其是在頁面改版,業務變動的過程中,舊的埋點不知道怎麼處理,新的埋點不知道需不需要,當埋點數量上來之後,對散落的埋點程式碼的維護是個災難。 當然你可以透過宏、工廠類去簡化埋點程式碼,但並不能改變什麼
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 埋点
APE(kEvent4001);
}
2、可視化埋點
可視化埋點,即透過可視化工具設定採集節點,在前端自動解析設定並上報埋點資料,從而實現所謂的“無痕埋點”, 代表方案是已經開源的 Mixpanel。
3、無埋點
無埋點或者叫做全埋點,它並不是真正的不需要埋點,而是採集全部事件上報。剩下交給伺服器做過濾,篩選出有用的資料。
無埋點進一步最佳化,可以透過伺服器下發設定檔案,直接由前端進行事件過濾。
為了 kpi,基於無埋點的思想,造了一個輪子 WKTrackingData。
這裡把 WKTrackingData 實現中碰到的一些問題與想法做一下記錄。
實現思想很簡單,所有程式碼如下:

其中 WKTrackingDataManager 負責所有的資料管理,事件追蹤設定。

WKTrackingDataViewPathHelper 負責 event_path 生成。
AOP 模塊負責所有事件的追蹤。
詳細的用法可以點擊 github 查看。
如何唯一標示某個事件?
這裡涉及兩個問題,一是事件怎麼表示?二是如何確保事件的唯一性?
事件的表示
對於事件的表示,使用了 event_path 實現。核心思想是對於觸發了某個事件的 responder ,順著其回應者鏈,構建出其回應者鏈的 path。
生成的回應者 path 如下:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
唯一標示某個事件
這裡有個問題是,某些業務場景下,同一個 button 或者其他控件,會因為其某些屬性的改變,在業務上表示的是多種不同的事件。
如,首頁一個 button 在未登入時顯示 點擊登入,登入未實名時顯示 去實名等等。
那麼對於這同一個 button 來說,它的視圖樹並未發生改變,生成的回應者 path 就是相同的。
類似的業務場景還有 UISwitch 的開關,UISegmentedControl 的 indexSelect,UIStepper ,以及 UITableView 和 UICollectionView 的 cell 點擊。
針對於這種情況,WKTrackingData 在生成 event_path 時,有選擇的將控件自身的不同屬性也拼接上,生成的 event_path 就變成了這樣:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
業務擴展
繼續考慮另外一種業務場景,首頁有一個 banner 輪播圖,
banner 每一個廣告位的圖片和跳轉 url 都是由伺服器下發的,且位置可設定。
這時 banner 的 每一個 index,對應什麼頁面都是不固定的,0 位所對應的事件,由 event_path 是無法確定的。
這時就需要拼接上具體的業務參數,才能夠唯一標示某個事件,如 url。
WKTrackingData 也提供了業務方的參數擴展,允許業務方拼接上自定義參數:

對於不希望進行事件追蹤的控件,可以透過 wk_ignoreTracking 進行忽略:
self.slider.wk_ignoreTracking = YES;
其他問題
在對於不同事件的追蹤上,WKTrackingData 基於面向切面的思想,使用 runtime 直接做方法交換。
但是對於 UIAlertView、UIActionSheet、UITableView、UICollectionView 的統計,需要交換其 delegate 的方法。
如果其
delegate class已經實現了相應的方法,那麼直接交換即可。如果其
delegate class未實現相應的方法,這時仍然想要追蹤到這些事件,那麼就需要手動添加一下對應的delegate method的實現。
- (void)wk_swizzleInstanceSelector:(SEL)origSel_ fromClass:(Class)fromClass replaceSelector:(SEL)replaceSel_ originNotImp:(SEL)notImpSel_ {
Method originalMethod = class_getInstanceMethod([self class], origSel_);
if (originalMethod) {
[self wk_swizzleInstanceSelector:origSel_ fromClass:fromClass replaceSelector:replaceSel_];
} else {
Method notImpMethod = class_getInstanceMethod(fromClass, notImpSel_);
// 如果delegateClass没有实现 origSel_ 方法
// 则给delegateClass的 origSel_ 添加 orginReplaceMethod 的实现
BOOL didAddNotImpMethod =
class_addMethod([self class],
origSel_,
method_getImplementation(notImpMethod),
method_getTypeEncoding(notImpMethod));
if (didAddNotImpMethod) {
NSLog(@"%@ did add not imp method %@" , NSStringFromClass([self class]) , NSStringFromSelector(notImpSel_));
}
}
}
但在實踐過程中發現,UITableView、UICollectionView 的 delegate 對象,在未實現相應方法時,手動給 tableView:didSelectRowAtIndexPath: 添加了 implementation,仍然不會觸發。
我們先來看一下,正常 tableView:didSelectRowAtIndexPath: 呼叫棧:

這裡面,UITableView 前後會呼叫這四個方法,進行 cell 點擊的回應。
- 1、[UITableView _userSelectRowAtPendingSelectionIndexPath:]
- 2、[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
- 3、[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
- 4、[TableViewController tableView:didSelectRowAtIndexPath:]
根據呼叫順序可以發現 UITableView 在真正呼叫 delegate class 的 tableView:didSelectRowAtIndexPath: 前,會先觸發自己的 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 方法。
直接給 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 打一個斷點進入匯編查看,發現:

如果 delegate class 如果有實現 tableView:didSelectRowAtIndexPath: ,則一切正常,會完成 tableView:didSelectRowAtIndexPath: 的呼叫。
但如果 delegate class 未實現 tableView:didSelectRowAtIndexPath: ,即便手動給 tableView:didSelectRowAtIndexPath: 添加 implementation,也不會嘗試回應 tableView:didSelectRowAtIndexPath: 了。
跟隨匯編呼叫發現,會直接 jump 到另外一個指令。
WKTrackingData 這裡換了種做法,直接對 [UITableView _userSelectRowAtPendingSelectionIndexPath:] 進行了交換(UICollectionView 類似)。 實現了 UITableView、UICollectionView 的 delegate 對象,在未實現相應方法時對 cell 事件的追蹤。
4、小結
多個event_path對應同一個事件
考慮另外一種情況,線上存在多個版本,不同版本的頁面都有一些細微的差別,那麼對於同一個事件來說,它就可能存在多個 event_path。
在實踐過程中,這一塊的映射,就需要伺服器同學的配合了,可以讓伺服器做成後台可設定的。
事件統計最佳化
在 業務擴展 中提到了如何添加業務參數,WKTrackingData 使用了分類實現。
在實踐過程中發現,全埋點的方案,造成了大量流量的浪費,有好多事件不需要啊[(ŏ_ŏ)]。
進一步最佳化,可以由伺服器下發設定檔案,裡面直接包含了,希望用戶端上報的所有事件,只有和設定檔案中符合的 event_path 才進行上報。
這一步的業務參數取得,也可以不採用 WKTrackingData 的實現,直接使用 kvc 取得。
形如:
key_path = "viewController.banner.url"
看到這裡就點開 WKTrackingData 給個star吧,有問題可以在guthub上提issue,或者下方評論~