iOS无埋点数据统计实验

项目中需要统计的数据包括

1.在某个页面的停留时间(针对UIViewController)
2.某个事件(method)触发的次数
3.某个View的展示次数

基本思路就是利用程序运行时动态创建类动态添加方法的思想。首先重写系统方法,然后自定义方法,在程序运行期间替换掉系统方法,达到全程序监控的效果。当然我们可能只需要针对某些特定的页面或者事件进行监听统计,那么解决方案就是配置一个json文件,里面包含要统计的页面和方法。
 

1.在某个页面的停留时间(针对控制层)

iOS为例:在页面展示和页面消失的时候都会走系统的两个方法viewDidAppearviewDidDisappear那么我们在运行期间将这两个方法hook到,不让他实现,转而替换成自定义的方法,那么在自定义的viewDidAppearu)中记录页面出现的时间,然后在viewDidDisappearu)中记录页面消失的时间,这样就完成了一次用于查看该页面时长的统计
分析:如何确定是那个页面被用户展示了,需要满足条件1.该页面类名在工程中是唯一的,该类名在json文件中对应的中文名是唯一的,这样就可以根据ClassName匹配到唯一的一个页面进行统计
 

2.某个事件(method)触发的次数 

同上在iOS中事件的触发底层的走的方法为sendAction:to:forEvent:,那么同理对该消息hook,让其执行的同时再走一个我们自定义的方法,也就是这个方法触发的次数就是自定义的方法触发的次数
 
分析:如何确定是那个页面被用户展示了,需要满足条件1.该方法target(执行者)的类名在工程中是唯一的,但与页面不同的是,假如我们通过循环创建的button,那么这些button拥有相同的ClassNamemethodName,这时候就需要通过tag值(也是一种控件的标识,默认值为0,那么匹配某个事件的时候就需要,先通过Class然后method,再匹配tag才能最终确定是那个控件被点击了。

1.停留时间runtime swap viewDidAppear & viewDidDisAppear
#import"UIViewController+JHswizzling.h"
staticNSDate *startDate;
@implementationUIViewController (JHswizzling)
+ (void)load{
   
   
   // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
   Method fromMethod = class_getInstanceMethod([selfclass],@selector(viewDidAppear:));
   Method toMethod = class_getInstanceMethod([selfclass],@selector(JH_swizzlingViewDidAppear));
   /**
     * 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
     * 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
     * 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
     */
   if (!class_addMethod([selfclass],@selector(JH_swizzlingViewDidAppear),method_getImplementation(toMethod),method_getTypeEncoding(toMethod))) {
       method_exchangeImplementations(fromMethod, toMethod);
    }
   
   // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
   Method fromMethodDis = class_getInstanceMethod([selfclass],@selector(viewDidDisappear:));
   Method toMethodDis = class_getInstanceMethod([selfclass],@selector(JH_swizzlingViewDidDisAppear));

   if (!class_addMethod([selfclass],@selector(JH_swizzlingViewDidDisAppear),method_getImplementation(toMethodDis),method_getTypeEncoding(toMethodDis))) {
       method_exchangeImplementations(fromMethodDis, toMethodDis);
    }
}

//我们自己实现的方法,也就是和selfviewDidAppear方法进行交换的方法。
- (void)JH_swizzlingViewDidAppear{
   
   NSString *str = [NSStringstringWithFormat:@"%@",self.class];
   // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
   NSDictionary *data = [selfgetJsonData];
   if ([[dataallKeys]containsObject:str]) {
       
       if(![strcontainsString:@"UI"]){
           startDate = [NSDate date];
           NSLog(@"统计打点出现: %@ time : %@", [selfgetJsonData][str] ,startDate);
        }
    }
   
    [selfJH_swizzlingViewDidAppear];
}

//我们自己实现的方法,也就是和selfviewDidDisAppear方法进行交换的方法。
- (void)JH_swizzlingViewDidDisAppear{
   NSString *str = [NSStringstringWithFormat:@"%@",self.class];
   // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
   NSDictionary *data = [selfgetJsonData];
   if ([[dataallKeys]containsObject:str]) {
       if(![strcontainsString:@"UI"]){
           //计算时间差
           NSDate *endDate = [NSDatedate];
           NSTimeInterval duration = [endDate timeIntervalSinceDate:startDate];
           NSLog(@"统计打点出现: %@ time : %f 时长", data[str] ,duration);
           //组合数据并存入数据库
           NSDictionary *vcDic = @{@"viewControllerCodeName":str,
                                      @"viewControllerName":data[str],
                                      @"viewControllerTime":[NSStringstringWithFormat:@"%f",duration],
                                      };
            [JH_AnalyseDataHelper_AnalyseWithData:vcDicwithType:AnalyseTypeViewController];
           
        }
    }
   
    [selfJH_swizzlingViewDidDisAppear];
}

-(NSDictionary*)getJsonData{
   NSString *filePath = [[NSBundlemainBundle]pathForResource:@"analyse"ofType:@"json"];
   NSData *jsonData = [NSDatadataWithContentsOfFile:filePath];
   NSDictionary *dic = [NSJSONSerializationJSONObjectWithData:jsonDataoptions:NSJSONReadingMutableLeaveserror:nil];
   
   return dic[@"viewController"];
}

2.某个事件(method)触发的次数,针对系统控件
#import"JHAnalyseControlAnalyseNode.h"

@implementationJHAnalyseControlAnalyseNode
+(void)load{
   Method JH_sendAction = class_getInstanceMethod([UIControlclass],@selector(sendAction:to:forEvent:));
   class_addMethod([UIControlclass],@selector(JHhook_sendAction:to:forEvent:),method_getImplementation(JH_sendAction),method_getTypeEncoding(JH_sendAction));
   method_setImplementation(JH_sendAction,class_getMethodImplementation([selfclass],@selector(JHhook_sendAction:to:forEvent:)));
   
   
}
/**
 替换的方法
 */
-(void)JHhook_sendAction:(SEL)action to:(nullableid)target forEvent:(nullableUIEvent *)event{
   
   NSString *methodName = NSStringFromSelector(action);
   NSString *className = [NSStringstringWithUTF8String:object_getClassName(target)];
   UIControl *sender = (UIControl*)self;
   //第一层,视图ClassName
   NSDictionary *data = [[JHAnalyseControlAnalyseNodeclass]getJsonData];
   if ([[dataallKeys]containsObject:className]) {
       //第二层,Action
       NSDictionary *class = data[className];
       if([[classallKeys]containsObject:methodName]){
           NSDictionary *action = class[methodName];
           
           NSString *tag = [NSStringstringWithFormat:@"%ld",sender.tag];
           if([[actionallKeys]containsObject:tag]){
               NSDictionary *oneAction = action[tag];
               NSLog(@"mtthodName=%@,className=%@,classRealName=%@tag=%@",methodName,className,oneAction[@"name"],tag);
               //使用当前时间表示最后操作时间
               NSDate *date = [NSDatedate];
               
               NSTimeZone *zone = [NSTimeZonesystemTimeZone];
               
               NSInteger interval = [zone secondsFromGMTForDate: date];
               
               NSDate *localeDate = [date  dateByAddingTimeInterval: interval];
               
               //组合数据并存入数据库
               NSDictionary *eventDic = @{@"eventClass":className,
                                          @"eventCodeName":methodName,
                                          @"eventCount":@"1",
                                          @"eventDate":[NSStringstringWithFormat:@"%@",localeDate],
                                          @"eventName":oneAction[@"name"],
                                          @"eventTag":tag,
                                          @"eventUser":@"jianghong",
                                          };
                [JH_AnalyseDataHelper_AnalyseWithData:eventDicwithType:AnalyseTypeEvent];
            }
        }
    }
   
    [selfJHhook_sendAction:actionto:targetforEvent:event];
   
}
+(NSDictionary*)getJsonData{
   NSString *filePath = [[NSBundlemainBundle]pathForResource:@"analyse"ofType:@"json"];
   NSData *jsonData = [NSDatadataWithContentsOfFile:filePath];
   NSDictionary *dic = [NSJSONSerializationJSONObjectWithData:jsonDataoptions:NSJSONReadingMutableLeaveserror:nil];
   
   return dic[@"event"];
}
//配置的json文件
{
   "viewController":{
       "JHChatBaseController":"聊天主页面",
       "JHMapLocationVC":"定位",
       "JHChildMessageVC":"消息",
       "JHChildFriendsVC":"好友",
       "JHNoteVC":"广场",
       "JHSquareVC":"笔记"
    },
   "event":{
       "JHInputView":{
           "_additionButtonAction:":{
               "0":{
                   "class":"JHInputView",
                   "event":"_additionButtonAction:",
                   "tag":"0",
                   "name":"录音"
                },
               "1":{
                   "class":"JHInputView",
                   "event":"_additionButtonAction:",
                   "tag":"1",
                   "name":"相册"
                },
               "2":{
                   "class":"JHInputView",
                   "event":"_additionButtonAction:",
                   "tag":"2",
                   "name":"相机"
                },
               "3":{
                   "class":"JHInputView",
                   "event":"_additionButtonAction:",
                   "tag":"3",
                   "name":"定位"
                }
            },
           "_sendAction":{
               "0":{
                   "class":"JHInputView",
                   "event":"_sendAction",
                   "tag":"0",
                   "name":"发送信息"
                }
            }
        },
       "JHChatBaseCellVoice":{
           "onPlayButton:":{
               "0":{
                   "class":"JHChatBaseCellVoice",
                   "event":"onPlayButton:",
                   "tag":"0",
                   "name":"播放录音"
                }
            }
        }
    }
}

3.CoreData数据库操作


#import"JH_AnalyseDataHelper.h"
#define kManagedObjectContext [JH_ChatMessageManager sharedInstance].managedObjectContext
#define JH_EventAnalyseData @"EventAnalyseData"
#define JH_ViewControllerAnalyseData @"ViewControllerAnalyseData"

@implementationJH_AnalyseDataHelper
+(void)_AnalyseWithData:(NSDictionary*)data withType:(AnalyseType)analyseType{
   if (analyseType==AnalyseTypeViewController) {
        [selfanalyseVCWithData:data];
    }elseif (analyseType ==AnalyseTypeEvent){
        [selfanalyseEventWithData:data];
    }
   
   
}
/**
 分析统计页面
 */
+(void)analyseVCWithData:(NSDictionary*)data{
   //判断是新建还是更新
   NSArray *list = [self_searchViewControllerData];
   //创建对应的类
   NSString *vcName = data[@"viewControllerName"];
   
   for (ViewControllerAnalyseData*vcModelin list) {
       if ([vcModel.viewControllerNameisEqualToString:vcName]) {
           //更新时长数据
            vcModel.viewControllerTime= vcModel.viewControllerTime+ [data[@"viewControllerTime"]floatValue];
            [[JH_ChatMessageManagersharedInstance]saveContext];//保存
           return;
        }
    }
   //创建一个新的
   ViewControllerAnalyseData *vcModel = [NSEntityDescriptioninsertNewObjectForEntityForName:JH_ViewControllerAnalyseDatainManagedObjectContext:kManagedObjectContext];
   for (NSString*str in [dataallKeys]) {
       if ([strisEqualToString:@"viewControllerTime"]) {
           float time = [data[str] floatValue];
            [vcModelsetValue:@(time)forKey:str];
           continue;
        }
        [vcModelsetValue:data[str]forKey:str];
    }
   
    [[JH_ChatMessageManagersharedInstance]saveContext];//保存
}
/**
 分析统计事件
 */
+(void)analyseEventWithData:(NSDictionary*)data{
   //判断是新建还是更新
   NSArray *list = [self_searchEventData];
   //创建对应的类
   NSString *eventName = data[@"eventName"];
   
   for (EventAnalyseData*vcModelin list) {
       if ([vcModel.eventNameisEqualToString:eventName]) {
           //更新点击次数数据
            vcModel.eventCount= vcModel.eventCount+ [data[@"eventCount"]integerValue];
            [[JH_ChatMessageManagersharedInstance]saveContext];//保存
           return;
        }
    }
   
   //创建一个新的
   EventAnalyseData *eventModel = [NSEntityDescriptioninsertNewObjectForEntityForName:JH_EventAnalyseDatainManagedObjectContext:kManagedObjectContext];
   
   for (NSString*str in [dataallKeys]) {
       if ([strisEqualToString:@"eventCount"]) {
           NSInteger count = [data[str] integerValue];
            [eventModelsetValue:@(count)forKey:str];
           
           continue;
        }
        [eventModelsetValue:data[str]forKey:str];
    }
   
    [[JH_ChatMessageManagersharedInstance]saveContext];//保存
}

#pragma mark -查询数据(暂时使用全部搜索)
+(NSArray*)_searchViewControllerData{
   /**
    数据查询数据(全部)
     */
   NSFetchRequest *request = [[NSFetchRequestalloc]init];
   
   NSEntityDescription *entity = [NSEntityDescriptionentityForName:JH_ViewControllerAnalyseData
                                  
                                             inManagedObjectContext:kManagedObjectContext];
   
    [requestsetEntity:entity];
   
   NSError *error = nil;
   
   NSArray *objectResults = [kManagedObjectContext
                             
                             executeFetchRequest:request
                             
                             error:&error];
   return objectResults;
   
}
+(NSArray*)_searchEventData{
   /**
    数据查询数据(全部)
     */
   NSFetchRequest *request = [[NSFetchRequestalloc]init];
   
   NSEntityDescription *entity = [NSEntityDescriptionentityForName:JH_EventAnalyseData
                                  
                                             inManagedObjectContext:kManagedObjectContext];
   
    [requestsetEntity:entity];
   
   NSError *error = nil;
   
   NSArray *objectResults = [kManagedObjectContext
                             
                             executeFetchRequest:request
                             
                             error:&error];
   return objectResults;
   
}

关于发送数据到服务端时间:进入后台的时候

4.最新优化:数据库表结构

数据统计汇总表(data_tracking)


idid

resource安装包来源AppStore/360市场…
deviceType设备类型iphone-6s-10.3.2/华为-mate9-6.0…string
userId用户Id
long
userName用户名称
string
eventCount本次统计事件数量
long
viewCount本次统计页面数量
long
startTime用户打开APP时间启动APP时间戳long
closeTime用户关闭APP时间关闭APP时间戳long
dataIndex第几次统计数据用于关联页面和事件表(APP启动时确定)long

页面数据统计表(viewData_tracking)


idid

dataIndex第几次统计数据用于关联页面和事件表(APP启动时确定)long
viewClassName页面对应的Class名应该是唯一的,但安卓和iOS是不同的string
viewName页面中文统计名称应该是唯一的string
duration启动APP到进入后台这个页面展示的总时长每次进入页面离开页面累加一次float
备注:每次根据ViewName和dataIndex是否相同进行数据的插入和更新



页面数据统计表(actionData_tracking)


idid

dataIndex第几次统计数据用于关联页面和事件表(APP启动时确定)long
targetClassName事件执行者对应的Class名
string
actionName统计时事件的名称
string
actionMethodName事件代码中的方法名称
string
actionTag由于一个target中有多个action,同个action下有多个控件,故设置tag作为唯一区分根据target、method、tag才能唯一确定一个控件是什么string
actionCount从APP启动到进入后台某个按钮点击的总次数每次累加
备注:每次根据target、method、tag和dataIndex是否相同进行数据的插入和更新


数据库的统计结果


5.最终传输给服务器的

{
    "resource":"AppStore",
    "deviceType":"iphone-6s-10.3.2",
    "userId":110,
    "userName":"江弘",
    "eventCount":6,
    "viewCount":10,
    "startTime":1499999391,
    "closeTime":1499999395,
    "dataIndex":1,
    "view":[

        {

            "viewClassName":"CFHomeViewController",

            "viewName":"首页",

            "duration":100

        },

        {

            "viewClassName":"CFUserViewController",

            "viewName":"用户中心",

            "duration":100

        }

    ],
    "action":[

        {

            "targetClassName":"CFBannerView",

            "actionName":"点击轮播图",

            "actionMethodName":"_bannerAction",

            "actionTag":"1",

            "actionCount":10

        },

        {

            "targetClassName":"CFInputView",

            "actionName":"点击发送文字",

            "actionMethodName":"_sendTextAction",

            "actionTag":"0",

            "actionCount":5

        }

    ]
}

已有最新优化数据统计方案,有空再更新
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值