客戶端埋點(diǎn)大概分為三類:
- 代碼埋點(diǎn)
- 可視化埋點(diǎn)
- 無埋點(diǎn)
1、代碼埋點(diǎn)
代碼埋點(diǎn),即在需要埋點(diǎn)的節(jié)點(diǎn)調(diào)用接口直接上傳埋點(diǎn)數(shù)據(jù),第三方數(shù)據(jù)統(tǒng)計(jì)服務(wù)商也大都提供了代碼埋點(diǎn)的 api,非常方便。
但是帶來一個(gè)問題,埋點(diǎn)代碼散落在業(yè)務(wù)的各個(gè)地方,和業(yè)務(wù)耦合嚴(yán)重,尤其是在頁面改版,業(yè)務(wù)變動(dòng)的過程中,舊的埋點(diǎn)不知道怎么處理,新的埋點(diǎn)不知道需不需要,當(dāng)埋點(diǎn)數(shù)量上來之后,對(duì)散落的埋點(diǎn)代碼的維護(hù)是個(gè)災(zāi)難。
當(dāng)然你可以通過宏、工廠類去簡化埋點(diǎn)代碼,但并不能改變什么
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 埋點(diǎn)
APE(kEvent4001);
}
2、可視化埋點(diǎn)
可視化埋點(diǎn),即通過可視化工具配置采集節(jié)點(diǎn),在前端自動(dòng)解析配置并上報(bào)埋點(diǎn)數(shù)據(jù),從而實(shí)現(xiàn)所謂的“無痕埋點(diǎn)”, 代表方案是已經(jīng)開源的 Mixpanel;
3、無埋點(diǎn)
無埋點(diǎn)或者叫做全埋點(diǎn),它并不是真正的不需要埋點(diǎn),而是采集全部事件上報(bào)。剩下交給服務(wù)器做過濾,篩選出有用的數(shù)據(jù)。
無埋點(diǎn)進(jìn)一步優(yōu)化,可以通過服務(wù)器下發(fā)配置文件,直接由前端進(jìn)行事件過濾。
為了 kpi,基于無埋點(diǎn)的思想,造了一個(gè)輪子 WKTrackingData。
這里把 WKTrackingData 實(shí)現(xiàn)中碰到的一些問題與想法做一下記錄。
實(shí)現(xiàn)思想很簡單,所有代碼如下:

其中 WKTrackingDataManager 負(fù)責(zé)所有的數(shù)據(jù)管理,事件追蹤配置。

WKTrackingDataViewPathHelper 負(fù)責(zé) event_path 生成。AOP 模塊負(fù)責(zé)所有事件的追蹤。
詳細(xì)的用法可以點(diǎn)擊 github 查看。
如何唯一標(biāo)示某個(gè)事件?
這里涉及兩個(gè)問題,一是事件怎么表示?二是如何確保事件的唯一性?
事件的表示
對(duì)于事件的表示,使用了 event_path 實(shí)現(xiàn)。核心思想是對(duì)于觸發(fā)了某個(gè)事件的 responder ,順著其響應(yīng)者鏈條,構(gòu)建出其響應(yīng)者鏈條的 path。
生成的響應(yīng)者 path 如下:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
唯一標(biāo)示某個(gè)事件
這里有個(gè)問題是,某些業(yè)務(wù)場景下,同一個(gè) button 或者其他控件,會(huì)因?yàn)槠淠承傩缘母淖?,在業(yè)務(wù)上表示的是多種不同的事件。
如,首頁一個(gè) button 在未登錄時(shí)顯示 點(diǎn)擊登錄,登錄未實(shí)名時(shí)顯示 去實(shí)名等等。
那么對(duì)于這同一個(gè) button 來說,它的視圖樹并未發(fā)生改變,生成的響應(yīng)者 path 就是相同的。
類似的業(yè)務(wù)場景還有 UISwitch 的開關(guān),UISegmentedControl 的 indexSelect,UIStepper ,以及 UITableView 和 UICollectionView 的 cell 點(diǎn)擊。
針對(duì)于這種情況,WKTrackingData 在生成 event_path 時(shí),有選擇的將控件自身的不同屬性也拼接上,生成的 event_path 就變成了這樣:
"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
業(yè)務(wù)擴(kuò)展
繼續(xù)考慮另外一種業(yè)務(wù)場景,首頁有一個(gè) banner 輪播圖,
banner 每一個(gè)廣告位的圖片和跳轉(zhuǎn) url 都是由服務(wù)器下發(fā)的,且位置可配置。
這時(shí) banner 的 每一個(gè) index,對(duì)應(yīng)什么頁面都是不固定的,0 位所對(duì)應(yīng)的事件,由 event_path 是無法確定的。
這時(shí)就需要拼接上具體的業(yè)務(wù)參數(shù),才能夠唯一標(biāo)示某個(gè)事件,如 url。
WKTrackingData 也提供了業(yè)務(wù)方的參數(shù)擴(kuò)展,允許業(yè)務(wù)方拼接上自定義參數(shù):

對(duì)于不希望進(jìn)行事件追蹤的控件,可以通過 wk_ignoreTracking 進(jìn)行忽略:
self.slider.wk_ignoreTracking = YES;
其他問題
在對(duì)于不同事件的追蹤上,WKTrackingData 基于面向切面的思想,使用 runtime 直接做方法交換。
但是對(duì)于 UIAlertView、UIActionSheet、UITableView、UICollectionView 的統(tǒng)計(jì),需要交換其 delegate 的方法。
如果其 delegate class 已經(jīng)實(shí)現(xiàn)了相應(yīng)的方法,那么直接交換即可。
如果其 delegate class 未實(shí)現(xiàn)相應(yīng)的方法,這時(shí)仍然想要追蹤到這些事件,那么就需要手動(dòng)添加一下對(duì)應(yīng)的 delegate method 的實(shí)現(xiàn)。
- (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沒有實(shí)現(xiàn) origSel_ 方法
// 則給delegateClass的 origSel_ 添加 orginReplaceMethod 的實(shí)現(xiàn)
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_));
}
}
}
但在實(shí)踐過程中發(fā)現(xiàn),UITableView、UICollectionView 的 delegate 對(duì)象,在未實(shí)現(xiàn)相應(yīng)方法時(shí),手動(dòng)給 tableView:didSelectRowAtIndexPath: 添加了 implementation,仍然不會(huì)觸發(fā)。
我們先來看一下,正常 tableView:didSelectRowAtIndexPath: 調(diào)用棧:

這里面,UITableView 前后會(huì)調(diào)用這四個(gè)方法,進(jìn)行 cell 點(diǎn)擊的響應(yīng)。
- 1、[UITableView _userSelectRowAtPendingSelectionIndexPath:]
- 2、[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
- 3、[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
- 4、[TableViewController tableView:didSelectRowAtIndexPath:]
根據(jù)調(diào)用順序可以發(fā)現(xiàn) UITableView 在真正調(diào)用 delegate class 的 tableView:didSelectRowAtIndexPath: 前,會(huì)先觸發(fā)自己的 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 方法。
直接給 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 打一個(gè)斷點(diǎn)進(jìn)入?yún)R編查看,發(fā)現(xiàn):

如果 delegate class 如果有實(shí)現(xiàn) tableView:didSelectRowAtIndexPath: ,則一切正常,會(huì)完成 tableView:didSelectRowAtIndexPath: 的調(diào)用。
但如果 delegate class 未實(shí)現(xiàn) tableView:didSelectRowAtIndexPath: ,即便手動(dòng)給 tableView:didSelectRowAtIndexPath: 添加 implementation,也不會(huì)嘗試響應(yīng) tableView:didSelectRowAtIndexPath: 了。
跟隨匯編調(diào)用發(fā)現(xiàn),會(huì)直接 jump 到另外一個(gè)指令。
WKTrackingData 這里換了種做法,直接對(duì) [UITableView _userSelectRowAtPendingSelectionIndexPath:] 進(jìn)行了交換(UICollectionView 類似)。
實(shí)現(xiàn)了 UITableView、UICollectionView 的 delegate 對(duì)象,在未實(shí)現(xiàn)相應(yīng)方法時(shí)對(duì) cell 事件的追蹤。
小結(jié)
多個(gè)event_path對(duì)應(yīng)同一個(gè)事件
考慮另外一種情況,線上存在多個(gè)版本,不同版本的頁面都都寫細(xì)微的差別,那么對(duì)于同一個(gè)事件來說,它就可能存在多個(gè) event_path。
在實(shí)踐過程中,這一塊的映射,就需要服務(wù)器同學(xué)的配合了,可以讓服務(wù)器做成后臺(tái)可配置的。
事件統(tǒng)計(jì)優(yōu)化
在 業(yè)務(wù)擴(kuò)展 中提到了如何添加業(yè)務(wù)參數(shù),WKTrackingData 使用了分類實(shí)現(xiàn)。
在實(shí)踐過程中發(fā)現(xiàn),全埋點(diǎn)的方案,造成了大量流量的浪費(fèi),有好多事件不需要啊[(?_?)]。
進(jìn)一步優(yōu)化,可以由服務(wù)器下發(fā)配置文件,里面直接包含了,希望客戶端上報(bào)的所有事件,只有和配置文件中符合的 event_path 才進(jìn)行上報(bào)。
這一步的業(yè)務(wù)參數(shù)獲取,也可以不采用 WKTrackingData 的實(shí)現(xiàn),直接使用 kvc 獲取。
形如:
key_path = "viewController.banner.url"
看到這里就點(diǎn)開 WKTrackingData 給個(gè)star吧,有問題可以在guthub上提issue,或者下方評(píng)論~