iOS 打點上報、無痕埋點

最近研習了美團等大廠的一些埋點方案。
還要感謝大神《xuhaoranLeo》的指點。(既然大神沒空寫博客、但我可以代勞哈)。

本文的宗旨是儘量全面、精簡、滿足我能想到儘量多的埋點需求。

主要通過以下這些方面來談談中埋點那些事:

  • 打點/上報的大概流程
  • 日誌記錄類型
  • 日誌應該帶有的數據
  • 打點的具體方式
  • 何時上報
  • 具體實現(iOS)

打點/上報的大概流程

  • 打點:當發生需要收集的行爲/狀態時、將其記錄在日記中。
  • 上報:選擇合適的時機將日誌上報。

日誌記錄類型

根據業務需要大致可以具有以下類型

  • 頁面/產品曝光
  • 用戶點擊
  • 性能打點(數據庫操作效率、APP運行卡頓)
  • 網絡監控

日誌應該帶有的數據

  • 一切分析時用得到的數據例子:
    行爲(點擊、瀏覽)、用戶(uid)、業務信息(gid、gtype)等。

  • 關鍵業務的性能監聽(其實性能打點我比較推薦單獨進行、畢竟這是開發關心的、產品分析並不需要):

  • 網絡請求失敗率、錯誤碼
  • 數據層操作耗時、App卡頓堆棧

打點的具體方式

代碼埋點:具體業務代碼處、手動添加埋點代碼。比如衡量圖片上傳、數據解析、OI操作的時間等

  • 聲明埋點: 通過將事件標識、業務字段作爲屬性添加在響應控件上。簡化代碼埋點的代碼量。
  • 無痕埋點:獲取全部操作、通過plist文件、決定需要上報的指定操作《美團:Mixpanel》
  • 無埋點:上報所有操作、由服務器篩選《GrowingIO》

具體實現:

由於每個項目的需求不同、具體實現也不一樣。
這裏只大概理順思路。

週期內記錄:

既然是統一上報、就需要在上報之前將本次週期中所有的指定操作記錄下來。

  • 每次操作中。由一個指定的模型(json)進行存儲。

  • 而整個週期中。我們採用一個單例、單例中有一個數組對單次模型進行存儲。

  /*
  * 數據存儲模型
  */
  
  @interface KTBehaviorData : NSObject
  @property (nonatomic, strong) NSString *op_type; // 1點擊事件 2頁面事件 3IO操作
  @property (nonatomic, strong) NSString *page_code; // 頁面Id
  @property (nonatomic, strong) NSString *event_code; // 事件Id
  @property (nonatomic, strong) NSDictionary *object_dic; // 內容Id
  @property (nonatomic, strong) NSString *op_time; // 點擊事件操作時間
  @property (nonatomic, strong) NSString *start_time; // 頁面事件開始時間
  @property (nonatomic, strong) NSString *end_time; // 頁面事件結束時間
  
  @end
  
  
  @interface KTBehaviorUpLoadData : NSObject
  @property (nonatomic, strong) NSString *app_type; //
  @property (nonatomic, strong) NSString *app_version;
  @property (nonatomic, strong) NSString *os_type; // 1蘋果iOS
  @property (nonatomic, strong) NSString *os_version; // 系統版本
  @property (nonatomic, strong) NSString *device_id; // 設備id
  @property (nonatomic, strong) NSString *user_id; // 用戶id
  @property (nonatomic, strong) NSString *login_account; // 用戶賬號
  @property (nonatomic, strong) NSString *screen; // 屏幕分辨率...
  @property (nonatomic, strong) NSMutableArray <KTBehaviorData *>*datas;
      
  @end

存儲&&上報:

在APP結束時歸檔存儲、APP啓動時上傳給服務器、上傳失敗則將歸檔數據重新寫入單例追加。

  • 寫入

    @implementation KTBehaviorDataManager
    + (void)load {
    
        //殺死程序 (但當程序位於後臺唄殺死不執行)
        __block id observer1 = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
            NSLog(@"殺死程序---將數據寫入本地");
            //將數據寫入本地
            [[KTBehaviorDataManager sharedManager] writeBehaviorData];
            
            [[NSNotificationCenter defaultCenter] removeObserver:observer1];
        }];
    
    
    //程序切換至後臺
        __block id observer2 = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
            NSLog(@"程序切換至後臺---將數據寫入本地");
            //將數據寫入本地
            [[KTBehaviorDataManager sharedManager] writeBehaviorData];
            
            [[NSNotificationCenter defaultCenter] removeObserver:observer2];
        }];
    }
    
  • 上傳

  • @implementation KTBehaviorDataUpLoader
    + (void)load {
      //程序啓動、上報記錄
      __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
      
          [KTBehaviorDataUpLoader upLoadData];
          [[NSNotificationCenter defaultCenter] removeObserver:observer];
      }];
    }
    

打點:

打點的方式有很多、但本質上都一樣。只是打點的代碼書寫位置不同而已。

這裏有一點需要注意一下:
在將捕獲的信息寫入manager的時候、記得加上安全保障。因爲整個app裏有很多地方都將會對manager進行操作、雖然出現資源搶奪的問題不大、但是並不代表永遠不會。

 #import "KTBehaviorDataManager.h"
  - (void)pushKTBehaviorDataWithModel:(KTBehaviorData *)model {
  
      //線程鎖、保證數據完整性
      @synchronized(self) {
        [self.data.datas addObject:model];
      }
  
  }

代碼埋點

看着多、但如果你把代碼封裝一下。就會發現少很多了

  - (void)submitBtnClick {
      KTBehaviorData *data = [[KTBehaviorData alloc] init];
      data.op_type = @"2";
      data.page_code = @"push";
      data.event_code = @"submitBtnClick";
      data.object_id = @{@"title":@"xx",@"content":@"xx"};
      data.op_time = [NSDate getCurrentTimeStamp];
      [[KTBehaviorDataManager sharedManager] pushKTBehaviorDataWithModel:data];
  }

當然、你可以把打點的方法抽離一下、更精簡一些而不使用Model。不過到了方法內部之後、都一樣。

  [[KTBehaviorDataManager sharedManager] pushKTBehaviorDataWithPageId:@"xxx" objectId:@"xxx"];

稍微高級點、一個記錄圖片上傳速度的埋點。

  - (void)upLoadPic {
  
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
          // Do the work in background
          KTBehaviorData * data = [KTBehaviorData new];
          data.op_type = @"3";
          data.page_code = @"ViewController";
          data.event_code = @"upLoadPic";
          data.start_time =[KTBehaviorData getNowTimeTimestamp];
          //圖片上傳
          [NSThread sleepForTimeInterval:5];
          
          data.end_time = [KTBehaviorData getNowTimeTimestamp];
          
          [[KTBehaviorDataManager sharedManager] pushKTBehaviorDataWithModel:data];
          NSLog(@"頁面IO埋點----%@",[data dicValue]);
      });
      
  }

這樣、就完成了一次提交按鈕被點擊的記錄。包括時間、控制器、事件、參數等。
只要你的模型結構足夠健壯、我們完全可以用一個模型記錄APP內的各種事件。

  • 結合剛纔說的寫入&&上報。大概這樣的效果

  • 上報


聲明埋點

通過runtime爲控件動態添加屬性。
然後在創建控件時爲屬性賦值。

  KTBehaviorData *parameter = [[KTBehaviorData alloc] init];
  parameter.bid = @"bid";
  parameter.lab = @{@"poi_id":@"1"};
  button.kt_clickParams = parameter;

然後在事件發生時進行記錄。


無痕埋點

簡而言之、有兩點。

  • 替換方法:通過swizzle對事件進行hook。
    這裏、我提供兩種方式。
  • 1、通過類別Hook原生方法:網上最普遍的方式。對event事件、table代理、頁面生命週期等方法進行Hook、但是無法直接對業務參數進行捕獲。

解決方案可以通過對NSObject擴展出一個打點專用結構體來獲取、但是本質上需要污染了業務代碼。

  • 2、hook指定Class中的指定方法:然後在指定方法中通過獲取class指定屬性值的方式捕獲參數。這要感謝《xuhaoranLeo》提供的方案。

在下文中我會對兩種方式進行說明並且舉例。

  • 篩選記錄:通過plist文件。通過文件名:pageId、方法名:enevtId等方式、自動爲模型參數賦值。

替換方法:

  • 通過類別Hook原生方法

現在還在這個階段大家對swizzle應用都比較頻繁了、沒什麼必要解釋太多。直接貼代碼吧

  • 舉個例子
    頁面進出、停留時間:

    @implementation UIViewController (KTHook)
    
    + (void)load {
        static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
        
              SEL originalSelector1 = @selector(viewWillAppear:);
              SEL swizzledSelector1 = @selector(kt_viewWillAppear:);
              [KTHook swizzlingInClass:[self class] originalSelector:originalSelector1 swizzledSelector:swizzledSelector1];
            
              SEL originalSelector2 = @selector(viewWillDisappear:);
              SEL swizzledSelector2 = @selector(kt_viewWillDisappear:);
              [KTHook swizzlingInClass:[self class] originalSelector:originalSelector2 swizzledSelector:swizzledSelector2];
          });
        }
    #pragma mark - Method Swizzling
    - (void)kt_viewWillAppear:(BOOL)animated
    {
      NSLog(@"進入");
    
      [[KTBehaviorDataManager sharedManager]pushKTBehaviorDataWithPageId:NSStringFromClass([self class]) time:[KTBehaviorData getNowTimeTimestamp]];
      [self kt_viewWillAppear:animated];
    }
    
    
    - (void)kt_viewWillDisappear:(BOOL)animated
    {
      NSLog(@"離開");
      [[KTBehaviorDataManager sharedManager]pushKTBehaviorDataWithPageId:NSStringFromClass([self class]) time:[KTBehaviorData getNowTimeTimestamp]];
      [self kt_viewWillDisappear:animated];
    }
    

頁面停留時間

同理、我們通過對UIControl的Event事件、UITableView代理等進行hook、進行無痕埋點。
具體方式網上有很多、千篇一律。我就不寫了、因爲不符合我想要獲取頁面參數的需求、貼出兩個教學帖想要這麼實現的可以自取。
《iOS 打點方案設計》《iOS動態性(二)可複用而且高度解耦的用戶統計埋點實現》

  • hook指定Class中的指定方法

思路就是上面寫的。實現的代碼也不難、hook過SDK文件的童鞋應該都知道。這裏爲了方便、我們用了一個封裝好的工具。《Aspects》

  @implementation NSObject (KTAspectsHook)

  + (void)load {
      __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
          [self setupBehaviorObj];
          [[NSNotificationCenter defaultCenter] removeObserver:observer];
      }];
  }
  
  #pragma mark - private method
  //hook所有需要打點的對象方法
  - (void)setupBehaviorObj {
  
      Class clazz = NSClassFromString(@"ViewController");
      //具體事件方法
      SEL selector = NSSelectorFromString(@"upLoadPic");
      
      [clazz aspect_hookSelector:selector withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {
          NSLog(@"ViewController中upLoadPic方法被調用、參數:aaa==%@",[[aspectInfo instance] valueForKey:@"aaa"]);
  
      } error:NULL];
      }
  
  @end

打印:

2018-01-24 14:43:21.419519+0800 kTBehaviorDemo[4587:363679] ViewController中upLoadPic方法被調用、參數:aaa==我是參數aaa

這樣、調用者、調用方法、參數。三大要素就都已經可以獲取到了。
但如何進行批量埋點?

篩選記錄

用plist、這個網上也很多帖子。之前提的兩個帖子也都提及了。

上段代碼可以修改如下:

  + (void)setupBehaviorObj {


      NSDictionary *behaviorPlist = [self getBehaviorEvents];

      for (NSString * className in behaviorPlist) {
          //需要hook的Class
          Class clazz = NSClassFromString(className);

          //對應Class需要hook的方法名
          NSDictionary *events = behaviorPlist[className];

          if (events[kBehaviorEvents]) {
              //事件數組
              for (NSDictionary *event in events[kBehaviorEvents]) {

                  //具體事件方法
                  SEL selector = NSSelectorFromString(event[kBehaviorEventSelectorName]);

                  [clazz aspect_hookSelector:selector withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {

                      //獲取參數
                      NSMutableDictionary * parameterDic = [NSMutableDictionary new];
                      if (event[kBehaviorParameter]) {

                          NSDictionary * dic = [NSObject properties_apsWithObj:[aspectInfo instance]];
                          for (NSString * parameterStr in event[kBehaviorParameter]) {
    
                              if ([dic valueForKey:parameterStr]) {
                                [parameterDic setValue:[dic valueForKey:parameterStr] forKey:parameterStr];
                              }
                          }
                      }

                      KTBehaviorData * data = [KTBehaviorData new];
                      data.op_time = [KTBehaviorData getNowTimeTimestamp];
                      data.event_code = event[kBehaviorEventId];
                      data.object_dic = parameterDic;
                      data.page_code = event[kBehaviorPageId];
                      data.op_type = event[kBehaviorType];

                      [[KTBehaviorDataManager sharedManager]pushKTBehaviorDataWithModel:data];


                  } error:NULL];

              }
          }
      }

  }

控制檯信息:

 

這樣、只要你的plist足夠健壯。確實可以做到幾乎完全無痕的埋點。

結束語:

《demo在此》

年前比較忙、但開了帖總要填完。所以可能有些錯別字和語法坑。

每個項目的需求不同、情況也不同。所以這只是個demo、希望能爲大家提供一個思路、並沒有封裝成一個SDK。

  • 不同的情況、可以用不同的打點方案。所謂無痕、並不一定是最好的、太暴力了。
  • 還有就是當項目很龐大的時候、進行hook操作、會不會影響性能。如果影響了、有沒有什麼改進的方式。
  • 如果你有什麼好的想法、或者是項目中有什麼更好的方案。還望指教。

補充:

經測。
當導入方法爲300時、肉眼無感。
當導入方法爲3000時、約1s。
當導入方法爲30000時、約15s。
由於在+load中加載、這段時間會算入app啓動白屏的時間內。


最後

本文主要是自己的學習與總結。如果文內存在紕漏、萬望留言斧正。如果不吝賜教小弟更加感謝。



作者:kirito_song
鏈接:https://www.jianshu.com/p/ddbfa8037e64
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章