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

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

  用户行为统计(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文件文件:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFuaGFpbG9uZzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

大家能够看到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前缀。

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

iOS打造属于自己的用户行为统计系统的更多相关文章

  1. ios/iphone手机请求微信用户头像错位BUG及解决方法

    转:http://www.jslover.com/code/527.html ios/iphone手机请求微信用户头像错位BUG及解决方法 发布时间:2014-12-01 16:37:01 评论数:0 ...

  2. iOS开发--应用设置及用户默认设置——转载

    [链接]iOS开发--应用设置及用户默认设置[1.bundlehttp://www.jianshu.com/p/6f2913f6b218 在iphone里面,应用都会在“设置”里面有个专属的应用设置, ...

  3. Oracle按用户进行统计信息更新

    按用户进行统计信息更新 PL/sqldev工具使用system用户连接到oracle,打开命令窗口执行以下SQL,用户名请根据实际情况进行更改: begin dbms_stats.gather_sch ...

  4. 【Linux_Fedora_系统管理系列】_1_用户登录和系统初始配置

    发现一个问题,在FC14 的Firefox浏览器中,编辑和排版好的博文,在windows下用chrome或者猎豹浏览器打开后,排版就变得阅读 不是很容易里,而且经常不经意的断行.不知道园子的管理人员时 ...

  5. 利用JS跨域做一个简单的页面访问统计系统

    其实在大部分互联网web产品中,我们通常会用百度统计或者谷歌统计分析系统,通过在程序中引入特定的JS脚本,然后便可以在这些统计系统中看到自己网站页面具体的访问情况.但是有些时候,由于一些特殊情况,我们 ...

  6. 网站流量统计系统 phpMyVisites

    phpMyVisites是一个网站流量统计系统,它能够提供非常详细的统计报告和高级图形报表.phpMyVisites不是一个Apache log分析工具,它建有自己的log.它的特点包括: 安装部署: ...

  7. 利用JS跨域做一个简单的页面訪问统计系统

    事实上在大部分互联网web产品中,我们一般会用百度统计或者谷歌统计分析系统,通过在程序中引入特定的JS脚本,然后便能够在这些统计系统中看到自己站点页面详细的訪问情况.可是有些时候,因为一些特殊情况,我 ...

  8. 1.6 flask应用: 代码统计系统

    2019-1-6 15:57:18 今天的是做了一个代码统计的demo 使用了数据库的连接池 参考连接 https://www.cnblogs.com/wupeiqi/articles/8184686 ...

  9. piwik网站访问统计系统

    一.Piwik介绍 Piwik是一套基于PHP+MySQL技术构建的开源网站访问统计系统.Piwik可以给你详细的统计信息,比如网页浏览人数,访问最多的页面,搜索引擎关键词等流量分析功能.此外,它还采 ...

随机推荐

  1. php获取js里的参数

    php获取js的值有如下方式: 1.php echo出js文件得到返回值,在gamemap.js文件中输出参数. echo '<script type="text/javascript ...

  2. 《Unix环境高级编程》读书笔记 第7章-进程环境

    1. main函数 int main( int argc, char *argv[] ); argc是命令行参数的数目,包括程序名在内 argv是指向参数的各个指针所构成的数组,即指针数组 当内核执行 ...

  3. 优动漫PAINT中误删工具怎么办?

    最近收到一些小伙伴的提问,说我不小心把 XXX工具从面板上删掉了怎么办?本教程就来给大家分 享一下遇到这个问题时的三种解决方法,遇到同样问题的小伙伴们赶紧进来看一下哟! 优动漫PAINT下载:http ...

  4. Git常见问题 资料汇总

    来源https://blog.csdn.net/albb_/article/details/80420468

  5. mysql联查中使用if和group by会让你的结果不是你想要的

    mysql中的if语句遇到统计count group by的时候会出现不准确的情况,原因是分组后if条件的结果以第一条为准,不会跟着分组 例如: SELECT t1.*,t2.nick_name,t2 ...

  6. 【codeforces 589G】Hiring

    [题目链接]:http://codeforces.com/problemset/problem/589/G [题意] 有n个人; 每个人每天在开始工作之前,都需要di单位的准备时间,然后才能开始工作; ...

  7. Login.hbm.xml

    <?xml version="1.0" encoding="utf-8"?><!DOCTYPE hibernate-mapping PUBLI ...

  8. 2015 Multi-University Training Contest 4 hdu 5336 XYZ and Drops

    XYZ and Drops Time Limit: 3000/1500 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Tot ...

  9. 华夏60 战斗机(最短路dijkstra)

    华夏60 战斗机(最短路dijkstra) 华夏60 超音速战斗机是当今世界上机动性能最先进的战斗机.战斗过程中的一个关键问题是如何在最短的时间内使飞机从当前的飞行高度和速度爬升/俯冲到指定的高度并达 ...

  10. 不安全的直接对象引用:你的 ASP.NET 应用数据是否安全?

    介绍 作为一个在X94的航空工程师,你的老板要求你从2号楼的工程图中检索出一个特定的专利.不幸的是,进入大楼需要你出示你具有进入大楼的资格的证明,然后你迅速地以徽章的形式出示给了保安.到了十三楼,进入 ...