iOS 统计打点记录
统计打点,意思就是在某个点击事件,或者函数调用上做一个记录,看一下这个函数调用的情况,简单的方式就是发送一个http请求
最近项目有做打点的需求,理了一下思路。
有很多实现的方式,比如在函数里直接发起http请求上报服务器,不过会对整个工程进行侵入。好处就是简单明了。但是在一些使用第三方的库上还是无法打点,
比如:
- (void)configFollow:(BOOL)isFollowed {
///记录关注事件
[xxx httpSendAction:@"Follow"];
[self followButtonStyle:isFollowed];
///做自己的事情
}
对要打点的类做分类,然后hook分类的方法,在分类里打点,这里只是要简单的做method swizzling
比如:
@implementation XXXPerson (Record)
+ (void)load {
///做method swizzling
///替换原来的buttonClicked 方法为r_buttonClicked,在r_buttonClicked 里面做打点统计
///
}
+ (void)r_buttonClicked:(UIButton *)button {
///
做打点统计
///
[self r_buttonClicked:button];
}
@end
可以在initialize
方法中做method swizzling
但是不能保证系统没有其他XXXPerson
的category 不实现 initialize
,实现了就会出现未知状况
不好的地方是每个要打点的类都要写一个分类。工程大了比较麻烦
在一个类里做打点,不用加分类。解除了对类的依赖。
比如:
@implementation PTVStatistics
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
///要打点的地方做函数交换
[self swizzleRootViewControllerMethod];
[self swizzleLiveHouseActionMethod];
[self swizzleWindowVideoViewMethod];
[self swizzleVideoConfohCellMethod];
[self swizzleLiveDicSelectMethod];
[self swizzleDecodeCellMethod];
[self swizzleWMPageControllerMethod];
[self swizzleGameTableViewCellMethod];
[self swizzlePTVHomeViewControllerMethod];
[self swizzlePTVChannelsManagerMethod];
[self swzzlePTVPersonalInfoViewMethod];
[self swzzlePTVRechargeViewControllerMethod];
[self swzzlePTVChatMessageViewMethod];
});
}
+ (void)swizzlePTVHomeViewControllerMethod {
//看有木有这个类
if (!NSClassFromString(@"PTVHomeViewController")) {
return;
}
SEL sel = NSSelectorFromString(@"channelActionWithButton:");
Method method = class_getInstanceMethod(NSClassFromString(@"PTVHomeViewController"), sel);
void(*originalImp)(id, SEL,id) = (void (*)(id, SEL,id)) method_getImplementation(method);
IMP adjustedImp = imp_implementationWithBlock(^void(id instance,id button) {
//在此处调用打点方法
});
//交换方法实现
method_setImplementation(method, adjustedImp);
}
///
此处还有一些类似的方法。做打点
///
可以看出,对原类不产生任何依赖,即使之前的类的方法不存在或者函数名改动了,这里依然能执行。不会崩溃,只是不打点了而已。不需要打点统计的时候直接删除某个函数就行。基本能满足所有打点统计的需求,缺点是,还没有后台配合,做成可以自动打点的工具class,比如做一个sdk,后台下发需要hook 哪些方法。自动完成,不要在工程里侵入。
动态下发表打点实现思路。
接着上面的思路。目标是可以实现一个根据全局的配置文件来读取那些函数需要打点统计,思路。调用方式如下:
全局配置的json文件
{
"type": "001",
"Classes": [
{
"class" : "LiveHouseController",
"method" : "viewDidLoad",
"action" : "10002",
"des" : "进入直播间"
},
{
"class" : "PTVHomeViewController",
"method" : "channelActionWithButton:",
"action" : "10017",
"des" : "点击XXXButton"
},
{
"class" : "PTVWindowVideoView",
"method" : "sigleTap:",
"action" : "10029",
"des" : "点击进入XX页面"
}
]
}
调用的代码如下:
BOOL __methodStatis(Class c, NSString *methodName, NSString *action, NSString *tag) {
if ([c isKindOfClass:[NSString class]]) {
c = NSClassFromString((NSString *)c);
}
if (!c || !methodName) {
return NO;
}
[c aspect_hookSelector:NSSelectorFromString(methodName)
withOptions:AspectPositionAfter usingBlock:^(id aspectInfo) {
[PTVStatistics __actionMsgId:action
Location:tag];
} error:NULL];
return YES;
}
+ (void)loadClassFromJsonFile {
NSString *path = [[NSBundle mainBundle] pathForResource:@"PandaTVStatic.geojson" ofType:@""];
if (!path) return;
NSData *data = [NSData dataWithContentsOfFile:path];
NSDictionary *info = [NSJSONSerialization JSONObjectWithData:data
options:0 error:nil];
[info[@"Classes"] enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL * _Nonnull stop) {
__methodStatis(obj[@"class"],
obj[@"method"],
obj[@"action"],
nil);
}];
}
classMethod
可以是一个全局配置文件,
class
要打点的类
method
要打点的函数
action
上报的数据
tag 附加参数 // 需要则自己实现
最初版本实现
BOOL __methodStatis(Class c, NSString *methodName, NSString *action, NSString *tag) {
if ([c isKindOfClass:[NSString class]]) {
c = NSClassFromString((NSString *)c);
}
SEL sel = NSSelectorFromString(methodName);
NSMethodSignature *signature = [c methodSignatureForSelector:sel];
__autoreleasing id instance = nil;
BOOL isClassMethod = signature != nil && sel != @selector(init);
if (!isClassMethod) {
instance = [c alloc];
signature = [c instanceMethodSignatureForSelector:sel];
}
/// 参数个数为 0
Method method = class_getInstanceMethod(c, sel);
if (signature.numberOfArguments == 2) {
void(*originalImp)(id, SEL) = (void (*)(id, SEL)) method_getImplementation(method);
IMP adjustedImp = imp_implementationWithBlock(^void(id instance) {
originalImp(instance, sel);
//////
//打点
/////
[PTVStatistics __actionMsgId:action
Location:tag];
});
method_setImplementation(method, adjustedImp);
return YES;
}
/// 参数个数为 1
if (signature.numberOfArguments == 3) {
const char *type = [signature getArgumentTypeAtIndex:2];
YYEncodingType yytype = YYEncodingGetType(type);
// 参数类型 为 id
if (yytype == YYEncodingTypeObject) {
void(*originalImp)(id, SEL,id) = (void (*)(id, SEL,id)) method_getImplementation(method);
IMP adjustedImp = imp_implementationWithBlock(^void(id instance,id obj) {
//////
//打点
/////
[PTVStatistics __actionMsgId:action
Location:tag];
originalImp(instance, sel,obj);
});
method_setImplementation(method, adjustedImp);
return YES;
}
// 参数类型为 float
if (yytype == YYEncodingTypeFloat) {
void(*originalImp)(id, SEL,CGFloat) = (void (*)(id, SEL,CGFloat)) method_getImplementation(method);
IMP adjustedImp = imp_implementationWithBlock(^void(id instance,CGFloat obj) {
//////
//打点
/////
[PTVStatistics __actionMsgId:action
Location:tag];
originalImp(instance, sel,obj);
});
method_setImplementation(method, adjustedImp);
return YES;
}
//////
// 以下还有若干个 IF
/////
}
////
//以下还有若干个IF 判断 参数更多的函数
////
return NO;
}
这里引入YYModel
的YYEncodingGetType
来判断参数类型,从而对应正确的函数签名(比如:void(*originalImp)(id, SEL)
),如果参数多了,并且参数类型多了,实现完整估计要死人。。if else 不知道要写多少个。我是不敢想想
匹配参数个数,匹配参数类型。。这不是一个 if else 可以搞定的,应该还有更好的思路。。要继续探索。
寻找了几个合理的方案。最后引入Aspects
完美的解决所有。。以后看了源码再详细分析。。
#import "Aspects.h"
BOOL __methodStatis(Class c, NSString *methodName, NSString *action, NSString *tag) {
if ([c isKindOfClass:[NSString class]]) {
c = NSClassFromString((NSString *)c);
}
[c aspect_hookSelector:NSSelectorFromString(methodName)
withOptions:AspectPositionAfter usingBlock:^(id aspectInfo) {
[PTVStatistics __actionMsgId:action
Location:tag];
} error:NULL];
return YES;
}
这样就只要全局配置一个list 然后遍历就OK,这里只是一个样例,实际做起来可能更加复杂。
微信团队有做过详细的使用和源码解析。
Aspects