?? 打造一款符合自己公司需求的用户行为统计系统, 相信是非常多运营人员的梦想, 也是开发人员对技术的的执着追求
以下我为大家分一享下自己为公司打造的用户行为统计系统
?? 用户行为统计 (User Behavior Statistics, UBS) 一直是移动互联网产品中不可缺少的环节, 也俗称埋点对于产品经理, 运营人员来说埋点当然是越多, 覆盖范围越广越好废话废话就不多少了, 这里我主要利用了 AOP 面向切片编程的思想来解决问题的參考博客: 參考博客地址? 首先声明, 我这里并没有全然照搬别人博客这里主要是顺着别人博客思路去走, 走进死胡同, 然后返璞归真, 用自己的思路去实现的
之所以把别人的思路写下来讨论就是为了说明思考的过程有时也非常重要
用户行为统计统计什么?
?? 我们经常说用户行为统计, 那么用户行为统计主要统什计么呢, 在我看来主要分为两类: 1, 页面统计: PV ;2, 事件统计: 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"
来源: http://www.bubuko.com/infodetail-2495746.html