hook forwardingTargetForSelector
方法调用流程
runtime中具体的方法调用流程大致如下:
1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
3.如果没找到,去父类指针所指向的对象中执行1,2.
4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。
5.如果没有重写拦截调用的方法,程序报错。
拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢?
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理:
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
拦截调用的整个流程即Objective-C的消息转发机制。其具体流程如下图:
message_forwarding.png
由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:
1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2、调用forwardingTargetForSelector让别的对象去执行这个函数
3、调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。
如果都不中,调用doesNotRecognizeSelector抛出异常。
unrecognized selector crash 防护方案
既然可以补救,我们完全也可以利用消息转发机制来做文章。那么问题来了,在这三个步骤里面,选择哪一步去改造比较合适呢。
这里我们选择了第二步forwardingTargetForSelector来做文章。原因如下:
resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
选择了forwardingTargetForSelector之后,可以将NSObject的该方法重写,做以下几步的处理:
- hook forwardingTargetForSelector 方法
- 添加白名单,限制hook的范围,排除内部类,并自定义需要hook的类
- 创建桩类 ForwardingTarget
- 为桩类动态添加对应的selector的imp,指向一个函数,返回 NSNull 对象
- 将消息转移到该桩类对象 ForwardingTarget 上
- 将 hook 掉的 crash 信息进行上报,方便发现问题,后期修复掉。
需要说明的是:如何排除内部类?
dladdr可获得一个函数所在模块,名称以及地址。(hook非系统类)
static struct dl_info app_info;
if (app_info.dli_saddr == NULL) {
dladdr((__bridge void *)[UIApplication.sharedApplication.delegate class], &app_info);
}
struct dl_info self_info = {0};
dladdr((__bridge void *)[self class], &self_info);
// ignore system class
if (self_info.dli_fname == NULL || strcmp(app_info.dli_fname, self_info.dli_fname)) {
return XXHookOrgin(aSelector);
}
创建forwardingTarget (桩类)---XXShieldStubObject
XXShieldStubObject *stub = [XXShieldStubObject shareInstance];
将消息转移到桩类来实现
[stub addFunc:aSelector];
- (BOOL)addFunc:(SEL)sel {
return __addMethod([XXShieldStubObject class], sel);
}
int smartFunction(id target, SEL cmd, ...) {
return 0;
}
static BOOL __addMethod(Class clazz, SEL sel) {
NSString *selName = NSStringFromSelector(sel);
NSMutableString *tmpString = [[NSMutableString alloc] initWithFormat:@"%@", selName];
int count = (int)[tmpString replaceOccurrencesOfString:@":"
withString:@"_"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, selName.length)];
NSMutableString *val = [[NSMutableString alloc] initWithString:@"i@:"];
for (int i = 0; i < count; i++) {
[val appendString:@"@"];
}
const char *funcTypeEncoding = [val UTF8String];
return class_addMethod(clazz, sel, (IMP)smartFunction, funcTypeEncoding);
}