打造一款符合自己公司需求的用户行为统计系统,相信是很多运营人员的梦想,也是开发人员对技术的的执着追求。下面我为大家分一享下自己为公司打造的用户行为统计系统。 用户行为统计 (User Behavior Statistics, UBS) 一直是移动互联网产品中必不可少的环节,也俗称埋点。对于产品经理,运营人员来说,埋点当然是越多,覆盖范围越广越好。废话废话就不多少了,这里我主要利用了 AOP 面向切片编程的思想来解决这个问题的。参考博客:参考博客地址 首先声明,我这里并没有完全照搬别人博客,这里主要是顺着别人博客思路去走,走进死胡同,然后返璞归真,用自己的思路去实现的。之所以把别人的思路写下来讨论,就是为了说明思考的过程有时也很重要。
我们常常说用户行为统计,那么用户行为统计主要统什计么呢,在我看来主要分为两类:1,页面统计:PV ;2,事件统计:Event。
页面统计就是就在用户进入某个页面的时候,进记行录保存;在用户离开某个页面的时候进行保存记录。在当适的时候将保存的数据发送给后台服务器。实现代码如下:
- [UIViewControlleraspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfterusingBlock:^(iddata){
- [self JKhandlePV:data status:JKUBSPV_ENTER];
- }error:nil];[UIViewControlleraspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfterusingBlock:^(iddata){
- [self JKhandlePV:data status:JKUBSPV_LEAVE];
- }error:nil];
很多博客贴出这样的代码以为就解决了问题,其实忽略了很大的一个问题,这样简粗单暴的去处理,会发现项中目所的有 UIViewCnotroller 的这两个方法
,
- viewDidAppear:
都被会 hook,造了成额外的性能开销,非常的不好。所以我边这进行了处理只针对要统的计页面进行 hook 操作。具现体实如下:
- viewDidDisappear:
- + (void)configPV{
- for (NSString*vcNamein[JKUBSshareInstance].configureData[JKUBSPVKey]) {Classtarget =NSClassFromString(vcName);
- [target aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfterusingBlock:^(iddata){
- [self JKhandlePV:data status:JKUBSPV_ENTER];
- }error:nil];[target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfterusingBlock:^(iddata){
- [self JKhandlePV:data status:JKUBSPV_LEAVE];
- }error:nil];}
- }
事件统计主要是在用户触发事件时进行记录保存,然后在合适的时候将记的录数据发送给后台服务器进行处理。按照文章开头参考博客所说,简单将件事分成了 UIButotn,UIControl,UIGestureRecognizer 以及点击 UITableView 单元格 cell 触发的事件,点击 UICollectionView 单元格 cell 触发的事件。 按照这个思路我首先对 UIButton,UIControl 触发的事件进行处理:
- + (void)configUIControlEvent{
- [UIControl aspect_hookSelector:@selector(sendAction:to:forEvent:) withOptions:AspectPositionAfter usingBlock:^(iddata){
- [selfJKHandleEvent:data];
- } error:nil];
- }
这个实现起来相对容易些,相信大家都有实现过。
对
触发的事件进行处理,比较麻烦 首先
- UIGestureRecognizer
是一个类簇,我们触发事件时的 tap,LongPress,swipe,pan 等手势发送事件是并不是发送事件的真正的类,我这边通过打断点的形式找到了发送事件的真正的类是:
- UIGestureRecognizer
发送事件的私有方法是:
- UIGestureRecognizerTarget
然后我就通过 hook 操作对手势触发的事件进行了处理:
- _sendActionWithGestureRecognizer:
- + (void)configGestureRecognizerEvent{
- Class UIGestureRecognizerTarget =NSClassFromString(@"UIGestureRecognizerTarget");
- [UIGestureRecognizerTarget aspect_hookSelector:@selector(_sendActionWithGestureRecognizer:) withOptions:AspectPositionAfter usingBlock:^(iddata){
- [selfJKHandleEvent:data];
- } error:nil];
- }
对手势触发的事件进行统计虽然困难,但还是实现了。 对于点击 UITableView 单元格 cell 触发的事件,点击 UICollectionView 单元格 cell 触发的事件。我这边以点击 UITableView 单元格 cell 触发的事件为例进行说明。假设
实现了
- JKBViewController
的代理方法
- UITableView
那么我的实现如下:
- tableView:didSelectRowAtIndexPath:
- + (void)configureDelegateEvent{
- [JKBViewController aspect_hookSelector:@selector(tableView:didSelectRowAtIndexPath:) withOptions:AspectPositionAfter usingBlock:^(iddata){
- [selfJKHandleEvent:data];
- } error:nil];
- }
通过这个实现我们能够做到对点击 UITableView 单元格 cell 触发的事件进行统计,但是顺着参博考客作者的思路这一步一步做下来,做到这里我内心有种不的妙感觉。
以下是参考的博客作者在开发的过程中遇到的问题
- 1,并不是所有的事件都是有继承自UIControl的空间来发出的,比如:手势,点击Cell。2,并不是所有的按钮点击了之后就立马需要埋点上传?可能在按钮的响应方法中经过了层层的if(){ } else{ }最后才需要埋点。4,对于代理方法该怎样处理?5,如果很多个按钮对应着一个事件该怎样处理?
其针实对第 1 点,我边这虽然梳理了很多类型的事件,但是仍然有很多没有被统计上,比如摇一摇触发的事件,计步器触发的事件,tabBar 点击触发的事件等,还很有多我可能没到想的事件,我现发如果按照作者的意图,按照事件触发的类型去一个一个的进行 hook 操作的话,工作两蛮大,而且还是会有遗漏的。尤是其涉及到有方些法苹果没有开放给开发者,我们进行处理的话比较麻烦。开员发人估被计要累死啊。 针对第 2 点,按作照者的意图,会现发点击之后里面还有层层的判断,如何绕过层层的判断呢?这个我会在接下来详细阐述。 针对第 4 点,我在上面已经实现过了。 针对第 5 点,在现实的情况中确实存在者不同的页面中,甚至相同的页面中不同的按钮对应着同一个事件这样的问题。如果按照参考博客作者的思路确实处理起来很是麻烦。
针对上面出现的困境,我在想有没有更好的办法去解决呢。首先想到我们统计用户操的作事件,并是不为了统计用户点击了某个按钮,或者进行了某个手势操作,调了用某个代理方法。而为是了统计用户进行这个操作的目的是什么,是为了购物,还是为了分享等。所以我就打破参考博客作者的思路,不再对按钮,手势,单元格选中等事件进行 hook,而是对用户的目的事件触发的方法进行 hook,事件就是事件,没有来源之分。也就是 hook 就提示的事件,中间层层的逻辑判断,我不需要考虑,我只考虑 hook 的目的事件。举例个子,用户要行进分享
,我不关心用是户否点击了按钮,或者 tap 手势触发了方法,或者单元格被中选,我只关心分享的方法
- - (void)goShare;
有没有被调用,被调用的时候我是否可以进记行录操作。另外唯一确定一个方法,除了 selector, 还要有相关的 target(方法的实现者,或者消息接受者)。针上面第 5 点,不同按钮对应同一个事件,一般情况下事件相同 target 不同,我们是能够区别的出来的。当了然也存在同一个页面上的不同按钮触发的同一个事件,这种情况下不是太常见,函数外面包一层,改个别的名字区分一下就好了,不过 EnvetID 还是要一样的。 为了更好的方便大家,我这边按自照己的思路写了一个 pod 库,下面先说一下自己的 plist 文件文件: 大家可以看到 PV 字段下,每一个页面都以可设置页面的名字,还一有些其他的信息。 Event 字段下有 EventID, 同时呢也允许同一个 EventID 下有不同的触发事件。 事件 1 这一级字段写上具体的事件内容,主要是方便开发人读员阅查找。 JKVC1 点击,JKVC2 点击,tap 单击,选中 tableView 单元格这些都是为了标件来明事源,方便开发人员阅读。另外如果事件还需要配置额外的参数,那么可以在 EventID 同级字段下添加新的内容。 下看看面来代码吧: JKUBS.h
- - (void)goShare;
- #import
- #import "Aspects.h"extern NSStringconst*JKUBSPVKey;
- extern NSStringconst*JKUBSEventKey;
- extern NSStringconst*JKUBSEventIDKey;
- extern NSStringconst*JKUBSEventConfigKey;
- extern NSStringconst*JKUBSSelectorStrKey;
- extern NSStringconst*JKUBSTargetKey;
- typedef NS_ENUM(NSInteger, JKUBSPVSTATUS){
- JKUBSPV_ENTER =0,//进入页面JKUBSPV_LEAVE//离开页面};@interfaceJKUBS : NSObject@property(nonatomic,strong,readonly) NSDictionary *configureData;/**
- 生成单例的方法 @return单例对象
- */+ (instancetype)shareInstance;/**
- 通过json配置文件导入配置信息
- json配置文件或plist配置文件只导入一个就好了 @paramjsonFilePath json文件沙盒路径
- */+ (void)configureDataWithJSONFile:(NSString *)jsonFilePath;/**
- 通过plist配置文件导入配置信息
- json配置文件或plist配置文件只导入一个就好了 @paramplistFileName plist文件名字(不带后缀名)
- */+ (void)configureDataWithPlistFile:(NSString *)plistFileName;/**
- 处理PV
- 这个方法需要开发者重载进行具体的操作 @paramdata 页面信息 @paramstatus 进入或离开页面的状态
- */+ (void)JKhandlePV:(id)data status:(JKUBSPVSTATUS)status;/**
- 处理事件
- 这个方法需要开发者重载进行具体的操作 @paramdata 事件信息 @parameventId 事件ID
- */+ (void)JKHandleEvent:(id)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
- staticJKUBS *_ubs =nil;
- + (instancetype)shareInstance{static dispatch_once_tonceToken;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) {
- [selfsetUp];
- }
- }
- + (void)configureDataWithPlistFile:(NSString*)plistFileName{NSDictionary*dic = [NSDictionarydictionaryWithContentsOfFile:[[NSBundlemainBundle] pathForResource:plistFileName ofType:@"plist"]];
- [JKUBS shareInstance].configureData= dic;if([JKUBS shareInstance].configureData) {
- [selfsetUp];
- }
- }
- + (void)setUp{
- [selfconfigPV];
- [selfconfigEvents];
- }#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:^(iddata){
- [selfJKhandlePV:data status:JKUBSPV_ENTER];
- } error:nil];
- [target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:AspectPositionAfter usingBlock:^(iddata){
- [selfJKhandlePV:data status:JKUBSPV_LEAVE];
- } error:nil];
- }
- }
- + (void)JKhandlePV:(id)data status:(JKUBSPVSTATUS)status{
- }#pragma mark EventConfig - - - -+ (void)configEvents{NSDictionary*eventsDic = [JKUBS shareInstance].configureData[JKUBSEventKey];NSArray*events =[eventsDic allValues];for(NSDictionary*dic in events) {NSIntegerEventID = [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:^(iddata){
- [selfJKHandleEvent:data EventID:EventID];
- } error:nil];
- }
- }
- }
- + (void)JKHandleEvent:(id)data EventID:(NSInteger)eventId{
- }
其中有两个方法要重点说一下。
- + (void)JKhandlePV:(id<AspectInfo>)datastatus:(JKUBSPVSTATUS)status;+ (void)JKHandleEvent:(id<AspectInfo>)data EventID:(NSInteger)eventId;
这两个方法都需要在 JKUBS 的 category 进行重载,来做具体的实现。例如页面活动的记录,事件的记录。打造用户行为统计系统,我这边已经完成了 AOP 思想下的事件采集,具体如何记录,保存,发给送后台,这里就不详细说明了。
代码下载地址 使用 pod 如下:
- pod"JKUBS"
来源: http://blog.csdn.net/hanhailong18/article/details/70140043