用戶端埋點大概分為三類:

  • 程式碼埋點
  • 可視化埋點
  • 無埋點

這篇文章主要介紹了 無埋點 的想法和實踐過程。

1、程式碼埋點

程式碼埋點,即在需要埋點的節點呼叫介面直接上傳埋點資料,第三方資料統計服務商也大都提供了程式碼埋點的 api,非常方便。

但是帶來一個問題,埋點程式碼散落在業務的各個地方,和業務耦合嚴重,尤其是在頁面改版,業務變動的過程中,舊的埋點不知道怎麼處理,新的埋點不知道需不需要,當埋點數量上來之後,對散落的埋點程式碼的維護是個災難。 當然你可以透過宏、工廠類去簡化埋點程式碼,但並不能改變什麼

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    // 埋点
    APE(kEvent4001);
}

2、可視化埋點

可視化埋點,即透過可視化工具設定採集節點,在前端自動解析設定並上報埋點資料,從而實現所謂的“無痕埋點”, 代表方案是已經開源的 Mixpanel。

3、無埋點

無埋點或者叫做全埋點,它並不是真正的不需要埋點,而是採集全部事件上報。剩下交給伺服器做過濾,篩選出有用的資料。

無埋點進一步最佳化,可以透過伺服器下發設定檔案,直接由前端進行事件過濾。

為了 kpi,基於無埋點的思想,造了一個輪子 WKTrackingData。

這裡把 WKTrackingData 實現中碰到的一些問題與想法做一下記錄。

實現思想很簡單,所有程式碼如下:

原始 WKTrackingData 架構:管理器、事件路徑與 AOP 模組

其中 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,或者下方評論~