iOS 的方法消息转发机制

274 阅读4分钟

奔溃的触发

我们声明一个类

#import "LGPerson.h"
@interface LGStudent : NSObject
- (void)sayHello;
+ (void)sayObjc;
@end

这个LGPerson类里面是没有saySomething这个方法的,加入我们强行调用的话

 LGStudent *student = [LGStudent alloc] ;
[student performSelector:@selector(saySomething)];

结果就是我们经常看到一种结果

[LGStudent saySomething]: unrecognized selector sent to instance 0x100737640
2020-03-10 13:17:10.039350+0800 008-方法查找-消息转发[8637:507477] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[LGStudent saySomething]: unrecognized selector sent to instance 0x100737640'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff2ffabf53 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff66071835 objc_exception_throw + 48
2 CoreFoundation 0x00007fff30036106 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff2ff526cb ___forwarding___ + 1427
4 CoreFoundation 0x00007fff2ff520a8 _CF_forwarding_prep_0 + 120
6 libdyld.dylib 0x00007fff673d42e5 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

从调用到崩溃这个过程还有没有抢救一下的余地呢?答案是可以

那么就到了我们要讲的 方法的消息转发

我在方法查找流程的文章中讲到过方法的查找流程,这里就不再过多陈述了 

我们这里只从方法在自身和父类都找不到的情况下开始整理

底层代码:

//自身和父类找不到相应的方法,走以下
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst); // 动态方法决议
runtimeLock.lock();
triedResolver = YES;
goto retry;
}

imp = (IMP)_objc_msgForward_impcache; //指向崩溃
cache_fill(cls, sel, imp, inst);

说明从找不到该方法到崩溃,苹果还是给我们留了一点余地的

动态方法决议

在LGPerson里面添加一个系统的动态决议方法我们验证他的走向

+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"毁灭你与你何干");
return [super resolveInstanceMethod:sel];
}


我们看到resolveInstanceMethod这个方法是走过了,但是崩溃没有制止,这次我们来制止他崩溃

+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"毁灭你与你何干:%s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(saySomething)) {
NSLog(@"说话了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}

上面这么写意思是,假如调用saySomething,我们就就把sayHello这个方法的调用给他

下面我们来做验证


是不是很神奇,直接不崩溃了

,实际开发中,我觉的可以这样做,假如用户触及特别的程序调用导致崩溃,我们可以给相应的提示,或者让他跳转到指定的界面

有人说每个类里面都写一个这样的方法是不是很麻烦,再者每个方法都判断一次是不是更麻烦

想一个东西大家应该会明白,那就是NSObject, 对,我们可以直接写到NSObject的分类里面,至于方法判断,大家可以统一把重点方法加一个共同字符串,例如'hiWorld',那么

        [NSStringFromSelector(Sel) containsString:@"hiWorld"];

这个判断足矣

消息的快速转发流程

假如做SDK的朋友防止调用崩溃在内部写了上述这么个代码,用户外层也写了其他的代码,这岂不是白写了吗,下面我们讲消息的快速转发流程

- (id)forwardingTargetForSelector:(SEL)aSelector{
return [super forwardingTargetForSelector:aSelector];
}


看上面信息,我可以看到已经不崩溃,但是问题来了,我是LGStudent 调用的,怎么跑到[LGTeacher saySomething]里面了?

上述转发流程含义: 我本身没有该方法,但是别的类有,我可以把这个方法其它的类来处理

消息的慢速转发流程

流程:假如该类没有该方法,我们先对该方法进行签名使其不崩溃,然后进行转发,交给指定类去调用,假如该类没有怎么不处理

// 慢速转发 -- 签名 - 转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"给文明以岁月%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomething)) { // v @ :
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//指定类处理 - 处理不了 - 失效
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"给岁月以文明 %s ",__func__);
SEL aSelector = [anInvocation selector];
if ([[LGTeacher alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[LGTeacher alloc]];
else
[super forwardInvocation:anInvocation];
}

下面调用打印

2020-03-10 14:26:36.086638+0800 008-方法查找-消息转发[9751:547644] 给文明以岁月-[LGStudent methodSignatureForSelector:] -- saySomething
2020-03-10 14:26:36.093111+0800 008-方法查找-消息转发[9751:547644] 给岁月以文明 -[LGStudent forwardInvocation:]
2020-03-10 14:26:36.093582+0800 008-方法查找-消息转发[9751:547644] -[LGTeacher saySomething]
Program ended with exit code: 0


消息转发流程图:

真正的大神的图,这里借用一下,嘿嘿


至此,消息转发流程叙述完毕,有什么问题,大伙可以一起留言讨论,多谢指教