项目中需要统计的数据包括
1.在某个页面的停留时间(针对UIViewController)
2.某个事件(method)触发的次数
3.某个View的展示次数
基本思路就是利用程序运行时动态创建类动态添加方法的思想。首先重写系统方法,然后自定义方法,在程序运行期间替换掉系统方法,达到全程序监控的效果。当然我们可能只需要针对某些特定的页面或者事件进行监听统计,那么解决方案就是配置一个json文件,里面包含要统计的页面和方法。
1.在某个页面的停留时间(针对控制层)
以iOS为例:在页面展示和页面消失的时候都会走系统的两个方法viewDidAppear和viewDidDisappear那么我们在运行期间将这两个方法hook到,不让他实现,转而替换成自定义的方法,那么在自定义的viewDidAppear(u)中记录页面出现的时间,然后在viewDidDisappear(u)中记录页面消失的时间,这样就完成了一次用于查看该页面时长的统计
分析:如何确定是那个页面被用户展示了,需要满足条件1.该页面类名在工程中是唯一的,该类名在json文件中对应的中文名是唯一的,这样就可以根据ClassName匹配到唯一的一个页面进行统计
2.某个事件(method)触发的次数
同上在iOS中事件的触发底层的走的方法为sendAction:to:forEvent:,那么同理对该消息hook,让其执行的同时再走一个我们自定义的方法,也就是这个方法触发的次数就是自定义的方法触发的次数
分析:如何确定是那个页面被用户展示了,需要满足条件1.该方法target(执行者)的类名在工程中是唯一的,但与页面不同的是,假如我们通过循环创建的button,那么这些button拥有相同的ClassName,methodName,这时候就需要通过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);
}
}
//我们自己实现的方法,也就是和self的viewDidAppear方法进行交换的方法。
- (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];
}
//我们自己实现的方法,也就是和self的viewDidDisAppear方法进行交换的方法。
- (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) | |||
id | id | ||
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) | |||
id | id | ||
dataIndex | 第几次统计数据 | 用于关联页面和事件表(APP启动时确定) | long |
viewClassName | 页面对应的Class名 | 应该是唯一的,但安卓和iOS是不同的 | string |
viewName | 页面中文统计名称 | 应该是唯一的 | string |
duration | 启动APP到进入后台这个页面展示的总时长 | 每次进入页面离开页面累加一次 | float |
备注:每次根据ViewName和dataIndex是否相同进行数据的插入和更新 |
页面数据统计表(actionData_tracking) | |||
id | id | ||
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
}
]
}
已有最新优化数据统计方案,有空再更新