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

打造一款符合自己公司需求的用户行为统计系统,相信是很多运营人员的梦想,也是开发人员对技术的的执着追求。下面我为大家分一享下自己为公司打造的用户行为统计系统。用户行为统计(User Behavior Statistics, UBS)一直是移动互联网产品中必不可少的环节,也俗称埋点。对于产品经理,运营人员来说,埋点当然是越多,覆盖范围越广越好。废话废话就不多少了,这里我主要利用了AOP面向切片编程的思想来解决这个问题的。
参考博客:参考博客地址。首先声明,我这里并没有完全照搬别人博客,这里主要是顺着别人博客思路去走,走进死胡同,然后返璞归真,用自己的思路去实现的。之所以把别人的思路写下来讨论,就是为了说明思考的过程有时也很重要。

用户行为统计统计什么?
  我们常常说用户行为统计,那么用户行为统计主要统什计么呢,在我看来主要分为两类:
1,页面统计:PV2,事件统计:Event
  页面统计:PV
  页面统计就是就在用户进入某个页面的时候,进记行录保存;在用户离开某个页面的时候进行保存记录。在当适的时候将保存的数据发送给后台服务器。实现代码如下:

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

        [UIViewController aspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfter 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]) {

        Class target = NSClassFromString(vcName);
        [target aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id data){
            [self JKhandlePV:data status:JKUBSPV_ENTER];
        } error:nil];

        [target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfter usingBlock:^(id data){
            [self JKhandlePV:data status:JKUBSPV_LEAVE];
        } error:nil];
    }


}

事件统计:Event
  事件统计主要是在用户触发事件时进行记录保存,然后在合适的时候将记的录数据发送给后台服务器进行处理。按照文章开头参考博客所说,简单将件事分成了UIButotn,UIControl,UIGestureRecognizer以及点击UITableView单元格cell触发的事件,点击UICollectionView单元格cell触发的事件。
  按照这个思路我首先对UIButton,UIControl触发的事件进行处理:

+ (void)configUIControlEvent{

    [UIControl aspect_hookSelector:@selector(sendAction:to:forEvent:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> data){
        [self JKHandleEvent:data];
    } error:nil];

}

这个实现起来相对容易些,相信大家都有实现过。
  对UIGestureRecognizer
触发的事件进行处理,比较麻烦 首先UIGestureRecognizer
是一个类簇,我们触发事件时的tap,LongPress,swipe,pan等手势发送事件是并不是发送事件的真正的类,我这边通过打断点的形式找到了发送事件的真正的类是:UIGestureRecognizerTarget
发送事件的私有方法是:_sendActionWithGestureRecognizer:
然后我就通过hook操作对手势触发的事件进行了处理:

+ (void)configGestureRecognizerEvent{ 
Class UIGestureRecognizerTarget =NSClassFromString(@"UIGestureRecognizerTarget"); 
[UIGestureRecognizerTarget aspect_hookSelector:@selector(_sendActionWithGestureRecognizer:) 
                                   withOptions:AspectPositionAfter 
                                    usingBlock:^(id<AspectInfo> 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:AspectPositionAfter 
                             usingBlock:^(id<AspectInfo> data){ 
                                 [self JKHandleEvent:data];
                             } error:nil
  ];
}

通过这个实现我们能够做到对点击UITableView单元格cell触发的事件进行统计,但是顺着参博考客作者的思路这一步一步做下来,做到这里我内心有种不的妙感觉。
走进死胡同
以下是参考的博客作者在开发的过程中遇到的问题

1,并不是所有的事件都是有继承自UIControl的空间来发出的,比如:手势,点击Cell。
2,并不是所有的按钮点击了之后就立马需要埋点上传?可能在按钮的响应方法中经过了层层的if(){ } else{ }最后才需要埋点。
3,对于代理方法该怎样处理?
4,如果很多个按钮对应着一个事件该怎样处理?

其针实对第1点,我边这虽然梳理了很多类型的事件,但是仍然有很多没有被统计上,比如摇一摇触发的事件,计步器触发的事件,tabBar点击触发的事件等,还很有多我可能没到想的事件,我现发如果按照作者的意图,按照事件触发的类型去一个一个的进行hook操作的话,工作两蛮大,而且还是会有遗漏的。尤是其涉及到有方些法苹果没有开放给开发者,我们进行处理的话比较麻烦。开员发人估被计要累死啊。 针对第2点,按作照者的意图,会现发点击之后里面还有层层的判断,如何绕过层层的判断呢?这个我会在接下来详细阐述。 针对第4点,我在上面已经实现过了。 针对第5点,在现实的情况中确实存在者不同的页面中,甚至相同的页面中不同的按钮对应着同一个事件这样的问题。如果按照参考博客作者的思路确实处理起来很是麻烦。
返璞归真
  针对上面出现的困境,我在想有没有更好的办法去解决呢。首先想到我们统计用户操的作事件,并是不为了统计用户点击了某个按钮,或者进行了某个手势操作,调了用某个代理方法。而为是了统计用户进行这个操作的目的是什么,是为了购物,还是为了分享等。所以我就打破参考博客作者的思路,不再对按钮,手势,单元格选中等事件进行hook,而是对用户的目的事件触发的方法进行hook,事件就是事件,没有来源之分。也就是hook就提示的事件,中间层层的逻辑判断,我不需要考虑,我只考虑hook的目的事件。举例个子,用户要行进分享- (void)goShare;
,我不关心用是户否点击了按钮,或者tap手势触发了方法,或者单元格被中选,我只关心分享的方法- (void)goShare;

有没有被调用,被调用的时候我是否可以进记行录操作。另外唯一确定一个方法,除了selector,还要有相关的target(方法的实现者,或者消息接受者)。针上面第5点,不同按钮对应同一个事件,一般情况下事件相同target不同,我们是能够区别的出来的。当了然也存在同一个页面上的不同按钮触发的同一个事件,这种情况下不是太常见,函数外面包一层,改个别的名字区分一下就好了,不过EnvetID还是要一样的。   为了更好的方便大家,我这边按自照己的思路写了一个pod库,下面先说一下自己的plist文件文件:
这里写图片描述
大家可以看到PV字段下,每一个页面都以可设置页面的名字,还一有些其他的信息。 Event字段下有EventID,同时呢也允许同一个EventID下有不同的触发事件。 事件1
这一级字段写上具体的事件内容,主要是方便开发人读员阅查找。 JKVC1点击,JKVC2点击,tap单击,选中tableView单元格这些都是为了标件来明事源,方便开发人员阅读。另外如果事件还需要配置额外的参数,那么可以在EventID同级字段下添加新的内容。 下看看面来代码吧:
JKUBS.h
#import <Foundation/Foundation.h>
#import "Aspects.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<AspectInfo>)data status:(JKUBSPVSTATUS)status;
/** 
  处理事件这个方法需要开发者重载进行具体的操作 
  @param data 事件信息 
  @param eventId 事件ID 
*/
+ (void)JKHandleEvent:(id<AspectInfo>)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 JKUBSstatic 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]) { 
        Class target = NSClassFromString(vcName);
         [target aspect_hookSelector:@selector(viewDidAppear:) 
                         withOptions:AspectPositionAfter 
                          usingBlock:^(id data){ 
                              [self JKhandlePV:data status:JKUBSPV_ENTER]; 
                          } error:nil ];
     [target aspect_hookSelector:@selector(viewDidDisappear:) 
                    withOptions:AspectPositionAfter 
                     usingBlock:^(id data){
                         [self JKhandlePV:data status:JKUBSPV_LEAVE]; 
                     } error:nil ]; 
    }
}

+ (void)JKhandlePV:(id<AspectInfo>)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:AspectPositionBefore 
                             usingBlock:^(id<AspectInfo> data){ 
                                [self JKHandleEvent:data EventID:EventID]; 
                             } error:nil ];
        } 
    }
}

+ (void)JKHandleEvent:(id<AspectInfo>)data EventID:(NSInteger)eventId{

}

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

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

这两个方法都需要在JKUBS的category进行重载,来做具体的实现。
例如页面活动的记录,事件的记录。
打造用户行为统计系统,我这边已经完成了AOP思想下的事件采集,具体如何记录,保存,发给送后台,这里就不详细说明了。
代码下载地址 使用pod如下:

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,611评论 4 59
  • 国庆假期仔细研读了Sam老师,张继生老师关于Lean/WCM不同视角的文章,对当前推行的WCM有了新的思考。尤其是...
    huanzichen阅读 1,610评论 0 1
  • 小时候第一次玩游戏就是爸妈带我入坑的,当时大概小学一年级,他们搬回来了一台小霸王,美其名曰是送我的礼物,大概长下面...
    miaowwww阅读 270评论 0 0
  • 白色柔顺灯光明 笼罩宁静又温馨 欣慰伴随白灯亮 吊顶空调风声起 轻声扰乱夜中静 床头回忆少年景 泪花有意湿眼睛 坎...
    卖菜诗人阅读 411评论 4 5