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

作者: kirito_song | 来源:发表于2018-01-18 15:33 被阅读292次

    最近研习了美团等大厂的一些埋点方案。
    还要感谢大神《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启动白屏的时间内。

    相关文章

      网友评论

      • 今天明天087:你好,请教一下,如果我想的在点击的cell的时候,进行数据埋点, 我hook掉tableview的代理点击方法,我怎么把cell的Model传过去呢,是通过定义好的plist文件通过KVO取值吗?
        今天明天087:@kirito_song OK 明白了 谢谢啊
        kirito_song:@今天明天087 获取VC的全部属性、拿到dataArr。根据代理方法里的index.row获取指定的model。
        如果觉得麻烦、把cell的点击事件挪到cell中hook。点击事件发生的时候将cell内的model传递过去就行了

      本文标题:iOS进阶补完计划--打点上报、无痕埋点

      本文链接:https://www.haomeiwen.com/subject/vsjwoxtx.html