一.应用背景
一般收集用户行为数据,埋点代码是在具体业务代码中实现,比如某个按钮的点击:
- (void)btnClick:(UIButton *)sender{
//埋点代码
[MobClick event:@"Home_Back")];
}
项目初期这样写是没有问题的,但是随着项目迭代,运营统计需求的增加会导致埋点代码到处都是,不便于管理。于是就需要考虑新方案---无侵入埋点方案。
二.无侵入埋点方案
无侵入埋点的优点在于:
- 埋点代码与业务代码剥离,降低耦合性
- 埋点方法集中统一管理,减少漏埋点的几率
三.实现原理
从上图中可以看出实现步骤只有四步:
- 调用
class_addMethod
为需要hook的类增加新方法,用新的SEL指向原有Method的原始IMP - 调用
method_setImplementation
为原有Method设置新IMP - 在新IMP中调用新SEL,实现原IMP。并收集对象,参数等信息传出给具体埋点业务模块。
- 在埋点业务模块中,加入埋点业务逻辑。
四.实现细节
1. 新IMP的实现
1.1 接收各种类型,数量的参数
void setMethodImplement(Method m,HookType type){
unsigned int count = method_getNumberOfArguments(m);
if (count<2 && count>10) {
return;
}
count = count-2;
switch (count) {
case 0:
SetHookImplement(m, 0, type);
break;
case 1:
SetHookImplement(m, 1, type);
break;
...
...
case 7:
SetHookImplement(m, 7, type);
break;
case 8:
SetHookImplement(m, 8, type);
break;
default:
break;
}
}
1.2 拼接函数
#define SetHookImplement(m,num,type)\
if(type==HookTypeNecessaryCallOrigin){\
method_setImplementation(m,(IMP)hookFunWith##num##Params);\
}else{\
method_setImplementation(m,(IMP)hookFuncWith##num##ParamsWithOutCallOrigin);\
}
1.3 不同参数个数函数实现
void *hookFunWith0Params(id self,SEL _cmd){
return hookFunc(self,_cmd);}
void *hookFunWith1Params(id self,SEL _cmd,void *param1){
return hookFunc(self,_cmd,param1);}
void *hookFunWith2Params(id self,SEL _cmd,void *param1,void *param2){
return hookFunc(self,_cmd,param1,param2);}
(省略)
void *hookFunWith8Params(id self,SEL _cmd,void *param1,void *param2,void *param3,void *param4,void *param5,void *param6,void *param7,void *param8){
return hookFunc(self,_cmd,param1,param2,param3,param4,param5,param6,param7,param8);}
1.4 新IMP(不定形参)
void* hookFunc(id self, SEL _cmd,...){
}
2. 在新IMP的调用原始IMP
2.1 根据新SEL生成方法签名,生成NSInvocation对象
NSMethodSignature *methodSig = [self methodSignatureForSelector:hookSelect];
if (methodSig == nil) {
return nil;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
2.2 使用va_list获取新IMP的参数列表,遍历,并赋值到NSInvocation对象中
id setInvocationParam(NSInvocation *invocation,int index,va_list *list,char *type){
if(strcmp(type, @encode(id)) == 0){
id param = va_arg(*list, id);
[invocation setArgument:¶m atIndex:index];
return param;
}
if (strcmp(type, @encode(NSInteger)) == 0 ||
strcmp(type, @encode(SInt8)) == 0 ||
strcmp(type, @encode(SInt16)) == 0 ||
strcmp(type, @encode(SInt32)) == 0 ||
strcmp(type, @encode(BOOL)) == 0) {
NSInteger param = va_arg(*list, NSInteger);
[invocation setArgument:¶m atIndex:index];
return @(param);
}
if(strcmp(type, @encode(CGFloat)) == 0 ||
strcmp(type, @encode(float)) == 0){
CGFloat param = va_arg(*list, double);
[invocation setArgument:¶m atIndex:index];
}
if(strcmp(type, @encode(void(^)())) == 0){
id param = va_arg(*list, id);
[invocation setArgument:¶m atIndex:index];
return param;
}
(--省略--)
return nil;
}
3. 埋点业务模块实现
创建AppDelegate分类,在分类中调用Method swizzling模块,将plist文件中需hook的类,方法信息一次性传入。在block回调中处理埋点业务。
五.存在的问题
1.va_list的问题
很多人包括JSPatch作者指出“一开始是用 va_list 的方式获取参数,结果发现 arm64 下不可用,才发现arm64下 va_list 的结构改变了,导致无法上述这样取参数”,详见文章。
但上述方案目前在64位机器上运行正常,也在上线版本中运行正常,没有发生crash问题,原因是什么?
2.父类实现问题
比如需在一些vc中hook
viewDidLoad
方法,但该vc未实现,则需去父类找实现方法,可能会导致父类被多次hook,引发死循环。
目前做法是强制被hook的类实现该方法,后续会参考Aspect,记录被hook状态,防止同一方法被多次hook。