iOS刨根问底栏目组程序员程序猿阵线联盟-汇总各类技术干货

iOS进阶补完计划--打点上报、无痕埋点

2018-01-18  本文已影响292人  kirito_song

最近研习了美团等大厂的一些埋点方案。
还要感谢大神《xuhaoranLeo》的指点。(既然大神没空写博客、但我可以代劳哈)。

本文的宗旨是尽量全面、精简、满足我能想到尽量多的埋点需求。

主要通过以下这些方面来谈谈中埋点那些事:


打点/上报的大概流程


日志记录类型

根据业务需要大致可以具有以下类型


日志应该带有的数据

  • 网络请求失败率、错误码
  • 数据层操作耗时、App卡顿堆栈

打点的具体方式


具体实现:

由于每个项目的需求不同、具体实现也不一样。
这里只大概理顺思路。

周期内记录:

既然是统一上报、就需要在上报之前将本次周期中所有的指定操作记录下来。

  /*
  * 数据存储模型
  */
  
  @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启动时上传给服务器、上传失败则将归档数据重新写入单例追加。

打点:

打点的方式有很多、但本质上都一样。只是打点的代码书写位置不同而已。

这里有一点需要注意一下:
在将捕获的信息写入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;

然后在事件发生时进行记录。


无痕埋点

简而言之、有两点。

  • 1、通过类别Hook原生方法:网上最普遍的方式。对event事件、table代理、页面生命周期等方法进行Hook、但是无法直接对业务参数进行捕获。

解决方案可以通过对NSObject扩展出一个打点专用结构体来获取、但是本质上需要污染了业务代码。

  • 2、hook指定Class中的指定方法:然后在指定方法中通过获取class指定属性值的方式捕获参数。这要感谢《xuhaoranLeo》提供的方案。

在下文中我会对两种方式进行说明并且举例。

替换方法:

现在还在这个阶段大家对swizzle应用都比较频繁了、没什么必要解释太多。直接贴代码吧

页面停留时间

同理、我们通过对UIControl的Event事件、UITableView代理等进行hook、进行无痕埋点。
具体方式网上有很多、千篇一律。我就不写了、因为不符合我想要获取页面参数的需求、贴出两个教学帖想要这么实现的可以自取。
《iOS 打点方案设计》《iOS动态性(二)可复用而且高度解耦的用户统计埋点实现》

思路就是上面写的。实现的代码也不难、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。

补充:

经测。
当导入方法为300时、肉眼无感。
当导入方法为3000时、约1s。
当导入方法为30000时、约15s。
由于在+load中加载、这段时间会算入app启动白屏的时间内。

上一篇下一篇

猜你喜欢

热点阅读