iOS Method swizzling实现无侵入埋点

一.应用背景

一般收集用户行为数据,埋点代码是在具体业务代码中实现,比如某个按钮的点击:

- (void)btnClick:(UIButton *)sender{
    //埋点代码
    [MobClick event:@"Home_Back")];
}

项目初期这样写是没有问题的,但是随着项目迭代,运营统计需求的增加会导致埋点代码到处都是,不便于管理。于是就需要考虑新方案---无侵入埋点方案。

二.无侵入埋点方案

无侵入埋点的优点在于:

  • 埋点代码与业务代码剥离,降低耦合性
  • 埋点方法集中统一管理,减少漏埋点的几率

三.实现原理

iOS Method swizzling实现无侵入埋点_第1张图片
方案.png

从上图中可以看出实现步骤只有四步:

  1. 调用class_addMethod为需要hook的类增加新方法,用新的SEL指向原有Method的原始IMP
  2. 调用method_setImplementation为原有Method设置新IMP
  3. 在新IMP中调用新SEL,实现原IMP。并收集对象,参数等信息传出给具体埋点业务模块。
  4. 在埋点业务模块中,加入埋点业务逻辑。

四.实现细节

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回调中处理埋点业务。


iOS Method swizzling实现无侵入埋点_第2张图片
hookPlist.png

五.存在的问题

1.va_list的问题

很多人包括JSPatch作者指出“一开始是用 va_list 的方式获取参数,结果发现 arm64 下不可用,才发现arm64下 va_list 的结构改变了,导致无法上述这样取参数”,详见文章。

但上述方案目前在64位机器上运行正常,也在上线版本中运行正常,没有发生crash问题,原因是什么?

2.父类实现问题

比如需在一些vc中hook viewDidLoad方法,但该vc未实现,则需去父类找实现方法,可能会导致父类被多次hook,引发死循环。

目前做法是强制被hook的类实现该方法,后续会参考Aspect,记录被hook状态,防止同一方法被多次hook。

你可能感兴趣的:(iOS Method swizzling实现无侵入埋点)