我们都知道是Objective-C是一门动态语言,只有在系统运行时(RunTime)才会根据函数的名称找的对应的函数来调用,我们通常这样[xxx doSomething]
来调用一个不带参数的函数,那么在系统运行的时候就会被转换为objc_msgSend(xxx,@selector(doSomething))
(xxx指接收消息的对象, @selector()是一个SEL方法选择器),如果是一个带参数的方法则会转换为
objc_msgSend(xxx,@selector(doSomething), arg1, arg2, ...)
.由此可以看出每一个Objective-C的函数中其实都自带了self
(这里的self指代接收对象)以及SEL
(方法_cmd)
在我们日常的开发中或多或少都会遇到"xxx unrecognized selector sent to instance 0x100....",这个异常信息,它通常是消息接收者找不到对应的@selector()方法.
但其实在这个异常抛出之前,系统给了我们几步来挽救:
· + (BOOL)resolveInstanceMethod:(SEL)sel; / + (BOOL)resolveClassMethod:(SEL)sel;
· - (id)forwardingTargetForSelector:(SEL)aSelector;
· - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
· - (void)forwardInvocation:(NSInvocation *)anInvocation;
其中在第一个方法中有两种方式分别对应该对象的Instance
与Class
,而第三个与第四个方法永远是成对出现的.他们的先后顺序就是1-4来执行的,总体分为三步1,2,(3-4),先来看看他们的一个总体流程:
整个的示例代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
DemoTest *test = [[DemoTest alloc] init];
[test performSelector:@selector(demoTest)];
}
return 0;
}
@interface DemoTest : NSObject
@end
@implementation DemoTest
@end
我们在main
函数中生成了一个DemoTest
并且调用demoTest
方法,但是从代码中可以看出DemoTest
类中并没有我们要找的方法,那么如果运行程序就会报错,接下来我们来逐步分析系统提供给我们的补救方法,这些方法都是写在DemoTest
中的:
+ (BOOL)resolveInstanceMethod:(SEL)sel; / + (BOOL)resolveClassMethod:(SEL)sel;
这是整个流程中的第一步,sel
参数是无法解析的方法名.在这个方法中我们可以动态的为消息的接收者添加这个sel
:
@implementation DemoTest
void demoTestMethod(id self, SEL _cmd)
{
NSLog(@"被调用...%@",NSStringFromSelector(_cmd));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *methodName = NSStringFromSelector(sel);
if ([methodName isEqualToString:@"demoTest"])
{
class_addMethod([self class], sel, (IMP)demoTestMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
我们首先获取这个sel
的名字,再来判断这个方法是不是需要我们动态添加的那个方法,如果是需要动态添加的就调用
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
方法来动态的为我们消息接收者添加方法:
-
cls
指的是消息接收者. -
name
就是无法解析的方法名. -
imp
表示要添加的函数IMP指针(指向函数的具体实现). -
types
是添加函数的类型.
这里需要解释下types
:这个参数就是定义函数返回值类型与参数类型的字符串(如果有参数的话).举个官方文档的例子- (BOOL)containsString:(NSString *)str
,转换为types
就是:c@:@,其中
- c 对应函数中的返回值(这里的返回值是BOOL),其余不同的返回值可以参考苹果官方文档,也可以通过打印@encode(type-name)来看看不同的返回值这里所对应的标识.
- @ 对应消息的接收者(self)
- : 对应SEL(_cmd)对象(containsString:)
- @ 对应函数中的参数(str)
这里其实第一步就走完了,如果在该函数内为指定的sel
提供实现,无论返回YES
还是NO
,编译运行都是可以通过的,但如果在该函数内并不真正为sel
提供实现,无论返回YES
还是NO
都会进入下一步.
- (id)forwardingTargetForSelector:(SEL)aSelector;
在第一步中如果接收者中没有实现对应的方法的话,就会进入这个函数,去寻找是否有别的对象可以接收这个消息,我们先创建一个新的类DemoObject
,并且在这个类中增加- (void)demoTest;
方法:
@interface DemoObject : NSObject
- (void)demoTest;
@end
@implementation DemoObject
- (void)demoTest
{
NSLog(@"这是DemoObject中的方法");
}
@end
接下来我们回到DemoTest
类中:
@implementation DemoTest
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"demoTest"])
{
DemoObject *demoObject = [[DemoObject alloc] init];
return demoObject;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
因为我们知道在DemoObject
中是有aSelector
方法的实现的,所以我们这里直接返回DemoObject
对象,如果这个函数的返回值为nil
的话,系统将继续进入到下一步.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)anInvocation;
这两个函数都成对出现的,也就是说系统给我们提供的补救方法中一共分为三步(这就是开始为什么要分为1,2,(3-4)).我们先来看看第一个方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
这个方法通过aSelector
参数返回了一个NSMethodSignature
对象,这个对象中包含一个方法中返回值与参数类型的信息,我们通常使用methodSignatureForSelector:
方法来创建,或者在
macOS 10.5以后的版本中我们可以使用signatureWithObjCTypes:
方法来创建.我们可以使用methodReturnType
属相来查看一个方法的返回值(更多).
@implementation DemoTest
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature)
{
if([DemoObject instancesRespondToSelector:aSelector])
{
signature = [DemoObject instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
@end
系统会根据这个signature
创建一个NSInvocation
对象作为参数传递给下一个方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
在iOS中,有两种方式可以调用SEL
,一个是performSelector:
系列的函数,还有个就是NSInvocation
.
NSInvocation
包含了一个消息中的所有信息,例如:接收对象,返回值,参数,SEL.我们也可以通过这个对象来进行消息的传递.
@implementation DemoTest
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([DemoObject instancesRespondToSelector:anInvocation.selector])
{
[anInvocation invokeWithTarget:[[DemoObject alloc] init]];
}
}
@end
我们通过anInvocation
中的selector
属性来判断DemoObject
中是否包含这个方法.如果有的话就就调用DemoObject
中的这个方法.
补充
在上面两个方法中我们都用到了instancesRespondToSelector:
方法, 我们除了这个方法外其实还熟悉另外一个respondsToSelector:
这两者都是用来判断某个方法是否存在,但区别在于:
- instancesRespondToSelector:用于类去判断实例方法是否存在.
- respondsToSelector:用于类判断类方法是否存在,实例判断实例方法是否存在.
我们再来看看一个普通的NSInvocation
是怎么工作的,这里搬一个网上找来的例子
- (void)viewDidLoad {
[super viewDidLoad];
SEL myMethod = @selector(myLog);
//创建一个函数签名,这个签名可以是任意的,但需要注意,签名函数的参数数量要和调用的一致。
NSMethodSignature * sig = [NSNumber instanceMethodSignatureForSelector:@selector(init)];
//通过签名初始化
NSInvocation * invocatin = [NSInvocation invocationWithMethodSignature:sig];
//设置target
[invocatin setTarget:self];
//设置selecteor
[invocatin setSelector:myMethod];
//消息调用
[invocatin invoke];
}
-(void)myLog{
NSLog(@"MyLog");
}
上面这个是不带参数的函数的调用方法,那么我们来看看带参数的调用方法:
- (void)viewDidLoad {
[super viewDidLoad];
SEL myMethod = @selector(myLog:parm:parm:);
NSMethodSignature * sig = [[self class] instanceMethodSignatureForSelector:myMethod];
NSInvocation * invocatin = [NSInvocation invocationWithMethodSignature:sig];
[invocatin setTarget:self];
[invocatin setSelector:myMethod];
int a=1;
int b=2;
int c=3;
[invocatin setArgument:&a atIndex:2];
[invocatin setArgument:&b atIndex:3];
[invocatin setArgument:&c atIndex:4];
[invocatin invoke];
}
-(void)myLog:(int)a parm:(int)b parm:(int)c{
NSLog(@"MyLog%d:%d:%d",a,b,c);
}
这里要说明以下的是为什么setArgument:
要从2开始,因为这个方法"翻译"成我们之前所说的types
的时候就是v@:i:i:i
前面的@与:都被占用了所以要2从开始.
那么如果以上三步都还没有完成补救的话,系统就会调用doesNotRecognizeSelector:
方法抛出异常了.