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启动白屏的时间内。


最后

本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果不吝赐教小弟更加感谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269