• iOS打造属于自己的用户行为统计系统


      打造一款符合自己公司需求的用户行为统计系统,相信是非常多运营人员的梦想,也是开发人员对技术的的执着追求。

    以下我为大家分一享下自己为公司打造的用户行为统计系统。


      用户行为统计(User Behavior Statistics, UBS)一直是移动互联网产品中不可缺少的环节,也俗称埋点。对于产品经理,运营人员来说。埋点当然是越多,覆盖范围越广越好。废话废话就不多少了,这里我主要利用了AOP面向切片编程的思想来解决问题的。參考博客:參考博客地址首先声明,我这里并没有全然照搬别人博客。这里主要是顺着别人博客思路去走,走进死胡同,然后返璞归真,用自己的思路去实现的。

    之所以把别人的思路写下来讨论。就是为了说明思考的过程有时也非常重要。

    用户行为统计统计什么?

      我们经常说用户行为统计,那么用户行为统计主要统什计么呢,在我看来主要分为两类:1,页面统计:PV2,事件统计:Event

    页面统计:PV

      页面统计就是就在用户进入某个页面的时候。进记行录保存。在用户离开某个页面的时候进行保存记录。

    在当适的时候将保存的数据发送给后台server。实现代码例如以下:

    [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){
                [self JKhandlePV:data status:JKUBSPV_ENTER];
            } error:nil];
    
            [UIViewController aspect_hookSelector:@selector(viewDidDisappear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){
                [self JKhandlePV:data status:JKUBSPV_LEAVE];
            } error:nil];
    

    非常多博客贴出这种代码以为就攻克了问题。事实上忽略了非常大的一个问题,这样简粗单暴的去处理,会发现项中目所的有UIViewCnotroller的这两个方法viewDidAppear:viewDidDisappear:都被会hook,造了成额外的性能开销,非常的不好。

    所以我边这进行了处理仅仅针对要统的计页面进行hook操作。具现体实例如以下:

    + (void)configPV{
        for (NSString *vcName in [[JKUBS shareInstance].configureData[JKUBSPVKey] allKeys]) {
    
            Class target = NSClassFromString(vcName);
            [target aspect_hookSelector:@selector(viewDidAppear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){
                [self JKhandlePV:data status:JKUBSPV_ENTER];
            } error:nil];
    
            [target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){
                [self JKhandlePV:data status:JKUBSPV_LEAVE];
            } error:nil];
        }
    
    
    }

    事件统计:Event

      事件统计主要是在用户触发事件时进行记录保存,然后在合适的时候将记的录数据发送给后台server进行处理。

    依照文章开头參考博客所说,简单将件事分成了UIButotn,UIControl,UIGestureRecognizer以及点击UITableView单元格cell触发的事件。点击UICollectionView单元格cell触发的事件。
      依照这个思路我首先对UIButton,UIControl触发的事件进行处理:

    + (void)configUIControlEvent{
    
        [UIControl aspect_hookSelector:@selector(sendAction:to:forEvent:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id<JKUBSAspectInfo> data){
            [self JKHandleEvent:data];
        } error:nil];
    
    }
    

    这个实现起来相对easy些,相信大家都有实现过。

      对UIGestureRecognizer触发的事件进行处理,比較麻烦 首先UIGestureRecognizer是一个类簇,我们触发事件时的tap,LongPress,swipe,pan等手势发送事件是并非发送事件的真正的类。我这边通过打断点的形式找到了发送事件的真正的类是:UIGestureRecognizerTarget 发送事件的私有方法是:_sendActionWithGestureRecognizer: 然后我就通过hook操作对手势触发的事件进行了处理:

    + (void)configGestureRecognizerEvent{
        Class UIGestureRecognizerTarget =NSClassFromString(@"UIGestureRecognizerTarget");
        [UIGestureRecognizerTarget aspect_hookSelector:@selector(_sendActionWithGestureRecognizer:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id<JKUBSAspectInfo> data){
            [self JKHandleEvent:data];
        } error:nil];
    
    
    }

    对手势触发的事件进行统计尽管困难,但还是实现了。
      对于点击UITableView单元格cell触发的事件,点击UICollectionView单元格cell触发的事件。我这边以点击UITableView单元格cell触发的事件为例进行说明。假设JKBViewController实现了UITableView 的代理方法tableView:didSelectRowAtIndexPath: 那么我的实现例如以下:

    + (void)configureDelegateEvent{
    
        [JKBViewController aspect_hookSelector:@selector(tableView:didSelectRowAtIndexPath:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id<JKUBSAspectInfo> data){
            [self JKHandleEvent:data];
        } error:nil];
    
    }
    

    通过这个实现我们能够做到对点击UITableView单元格cell触发的事件进行统计,可是顺着參博考客作者的思路这一步一步做下来。做到这里我内心有种不的妙感觉。

    走进死胡同

    以下是參考的博客作者在开发的过程中遇到的问题

    1。并非全部的事件都是有继承自UIControl的空间来发出的。比方:手势。点击Cell。
    2。并非全部的button点击了之后就立刻须要埋点上传?可能在button的响应方法中经过了层层的if(){ } else{ }最后才须要埋点。

    4,对于代理方法该如何处理? 5,假设非常多个button相应着一个事件该如何处理?

    其针实对第1点,我边这尽管梳理了非常多类型的事件。可是仍然有非常多没有被统计上,比方摇一摇触发的事件。计步器触发的事件。tabBar点击触发的事件等,还非常有多我可能没到想的事件。我现发假设依照作者的意图,依照事件触发的类型去一个一个的进行hook操作的话,工作两蛮大。并且还是会有遗漏的。尤是其涉及到有方些法苹果没有开放给开发人员,我们进行处理的话比較麻烦。

    开员发人估被计要累死啊。


    针对第2点,按作照者的意图,会现发点击之后里面还有层层的推断。如何绕过层层的推断呢?这个我会在接下来详细阐述。
    针对第4点。我在上面已经实现过了。
    针对第5点,在现实的情况中确实存在者不同的页面中。甚至同样的页面中不同的button相应着同一个事件这种问题。

    假设依照參考博客作者的思路确实处理起来非常是麻烦。

    返璞归真

      针对上面出现的困境。我在想有没有更好的办法去解决呢。

    首先想到我们统计用户操的作事件,并是不为了统计用户点击了某个button,或者进行了某个手势操作。调了用某个代理方法。而为是了统计用户进行这个操作的目的是什么。是为了购物。还是为了分享等。所以我就打破參考博客作者的思路,不再对button,手势。单元格选中等事件进行hook。而是对用户的目的事件触发的方法进行hook,事件就是事件,没有来源之分。也就是hook就提示的事件。中间层层的逻辑推断,我不须要考虑。我仅仅考虑hook的目的事件。

    举例个子,用户要行进分享- (void)goShare;,我不关心用是户否点击了button,或者tap手势触发了方法,或者单元格被中选。我仅仅关心分享的方法- (void)goShare;有没有被调用。被调用的时候我能否够进记行录操作。

    另外唯一确定一个方法。除了selector,还要有相关的target(方法的实现者,或者消息接受者)。针上面第5点,不同button相应同一个事件,普通情况下事件同样target不同,我们是能够差别的出来的。

    当了然也存在同一个页面上的不同button触发的同一个事件,这种情况下不是太常见,函数外面包一层。改个别的名字区分一下就好了。只是EnvetID还是要一样的。


      为了更好的方便大家。我这边按自照己的思路写了一个pod库。以下先说一下自己的plist文件文件:
    这里写图片描写叙述
    大家能够看到PV字段下,每个页面都以可设置页面的名字,还一有些其它的信息。
    Event字段下有EventID,同一时候呢也同意同一个EventID下有不同的触发事件。


    事件1这一级字段写上详细的事件内容,主要是方便开发人读员阅查找。


    JKVC1点击,JKVC2点击,tap单击。选中tableView单元格这些都是为了标件来明事源,方便开发人员阅读。另外假设事件还须要配置额外的參数。那么能够在EventID同级字段下加入新的内容。
    下看看面来代码吧:
    JKUBS.h

    #import <Foundation/Foundation.h>
    #import "JKUBSAspects.h"
    
    
    extern NSString const *JKUBSPVKey;
    extern NSString const *JKUBSEventKey;
    extern NSString const *JKUBSEventIDKey;
    extern NSString const *JKUBSEventConfigKey;
    extern NSString const *JKUBSSelectorStrKey;
    extern NSString const *JKUBSTargetKey;
    
    
    typedef NS_ENUM(NSInteger, JKUBSPVSTATUS){
        JKUBSPV_ENTER = 0,         //进入页面
        JKUBSPV_LEAVE              //离开页面
    };
    
    @interface JKUBS : NSObject
    
    @property (nonatomic,strong,readonly) NSDictionary *configureData;
    
    
    
    
    /**
     生成单例的方法
    
     @return 单例对象
     */
    + (instancetype)shareInstance;
    
    
    /**
     通过json配置文件导入配置信息
    json配置文件或plist配置文件仅仅导入一个就好了
     @param jsonFilePath json文件沙盒路径
     */
    + (void)configureDataWithJSONFile:(NSString *)jsonFilePath;
    
    
    /**
     通过plist配置文件导入配置信息
    json配置文件或plist配置文件仅仅导入一个就好了
     @param plistFileName plist文件名称字(不带后缀名)
     */
    + (void)configureDataWithPlistFile:(NSString *)plistFileName;
    
    
    /**
     处理PV
    这种方法须要开发人员重载进行详细的操作
     @param data 页面信息
     @param status 进入或离开页面的状态
     */
    + (void)JKhandlePV:(id<JKUBSAspectInfo>)data status:(JKUBSPVSTATUS)status;
    
    
    /**
     处理事件
    这种方法须要开发人员重载进行详细的操作
     @param data 事件信息
     @param eventId 事件ID
     */
    + (void)JKHandleEvent:(id<JKUBSAspectInfo>)data EventID:(NSInteger)eventId;
    
    
    @end
    

    JKUBS.m

    #import "JKUBS.h"
    
    NSString const *JKUBSPVKey = @"PV";
    NSString const *JKUBSEventKey = @"Event";
    NSString const *JKUBSEventIDKey = @"EventID";
    NSString const *JKUBSEventConfigKey = @"EventConfig";
    NSString const *JKUBSSelectorStrKey = @"selectorStr";
    NSString const *JKUBSTargetKey = @"target";
    
    
    @interface JKUBS()
    
    @property (nonatomic,strong,readwrite) NSDictionary *configureData;
    
    @end
    
    @implementation JKUBS
    static JKUBS *_ubs =nil;
    + (instancetype)shareInstance{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _ubs = [JKUBS new];
        });
    
        return _ubs;
    
    }
    
    + (void)configureDataWithJSONFile:(NSString *)jsonFilePath{
        NSData *data = [NSData dataWithContentsOfFile:jsonFilePath];
        NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
        [JKUBS shareInstance].configureData = dic;
    
        if ([JKUBS shareInstance].configureData) {
            [self setUp];
        }
    }
    
    
    + (void)configureDataWithPlistFile:(NSString *)plistFileName{
        NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:plistFileName ofType:@"plist"]];
        [JKUBS shareInstance].configureData = dic;
    
        if ([JKUBS shareInstance].configureData) {
            [self setUp];
        }
    
    }
    
    
    + (void)setUp{
    
        [self configPV];
        [self configEvents];
    
    }
    
    #pragma mark PVConfig - - - -
    
    + (void)configPV{
        for (NSString *vcName in [[JKUBS shareInstance].configureData[JKUBSPVKey] allKeys]) {
    
            Class target = NSClassFromString(vcName);
            [target aspect_hookSelector:@selector(viewDidAppear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){
                [self JKhandlePV:data status:JKUBSPV_ENTER];
            } error:nil];
    
            [target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){
                [self JKhandlePV:data status:JKUBSPV_LEAVE];
            } error:nil];
        }
    
    
    }
    
    
    
    + (void)JKhandlePV:(id<JKUBSAspectInfo>)data status:(JKUBSPVSTATUS)status{
    
    
    }
    
    #pragma mark EventConfig - - - -
    
    + (void)configEvents{
    
        NSDictionary *eventsDic = [JKUBS shareInstance].configureData[JKUBSEventKey];
        NSArray *events =[eventsDic allValues];
        for (NSDictionary *dic in events) {
            NSInteger EventID = [dic[JKUBSEventIDKey] integerValue];
            NSArray *eventConfigs = [dic[JKUBSEventConfigKey] allValues];
            for (NSDictionary *eventConfig in eventConfigs) {
                NSString *selectorStr = eventConfig[JKUBSSelectorStrKey];
                NSString *targetClass = eventConfig[JKUBSTargetKey];
                Class target =NSClassFromString(targetClass);
                SEL selector = NSSelectorFromString(selectorStr);
    
                    [target aspect_hookSelector:selector withOptions:JKUBSAspectPositionBefore usingBlock:^(id<JKUBSAspectInfo> data){
                        [self JKHandleEvent:data EventID:EventID];
                    } error:nil];
    
    
            }
        }
    
    
    }
    
    + (void)JKHandleEvent:(id<JKUBSAspectInfo>)data EventID:(NSInteger)eventId{
    
    }
    

    当中有两个方法要重点说一下。

    + (void)JKhandlePV:(id<JKUBSAspectInfo>)data status:(JKUBSPVSTATUS)status。
    + (void)JKHandleEvent:(id<JKUBSAspectInfo>)data EventID:(NSInteger)eventId;

    这两个方法都须要在JKUBS的category进行重载,来做详细的实现。

    比如页面活动的记录,事件的记录。打造用户行为统计系统。我这边已经完毕了AOP思想下的事件採集。详细如何记录,保存,发给送后台,这里就不详细说明了。

    代码下载地址
    使用pod例如以下:

    pod "JKUBS"

    注意:demo中我对aspects库进行了改动。为了防止名字冲突,我这边统一都加了JKUBS前缀。

    欢迎大家来找茬,一块交流学习。

  • 相关阅读:
    《构建之法》第1.2.3章读后感
    回顾并总结关于复利计算器的三次实验
    实验0、了解和熟悉操作系统实验
    0302思考并回答一些问题
    递归下降语法分析程序设计
    1203有穷自动机的构造与识别
    评论集锦
    C语言文法定义与C程序的推导过程
    Vue简介教程(四)[自定义指令 | 路由 | 过渡 & 动画]
    Vue简介教程(三)[事件处理 | 表单 | 组件]
  • 原文地址:https://www.cnblogs.com/zhchoutai/p/8443152.html
Copyright © 2020-2023  润新知