unrecognized selector
在(九)中,我们已经了解到了runtime的消息发送机制,某个函数实现方法的查找是依靠在运行期间不停的遍历类中的“方法列表”来查找的。具体还是看(九)。
但是当我们找不到方法的实现时,程序就会崩溃。比如执行以下代码:
NSString *testStr = @"test";
[testStr performSelector:@selector(addObject:) withObject:@"1"];
testStr不存在addObject:方法,所以程序抛出以下异常:
2016-05-05 17:37:22.827 RunTimePlayGround[23405:3123461] -[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x101e14150
在抛出这个异常之前,还是来看一下这个消息都经过了什么:
- 1 尝试动态给目标类(比如这个例子为NSString)添加一个方法来实现这个未知的selector。
- 2 尝试寻找另一个能够识别selector的receiver。
- 3 对消息使用NSInvocation对象进行重新包装,给receiver最后一次识别机会。
以上就是一个完整的消息转发机制。
消息转发机制
上图是一个消息转发机制的流程。下面详解。
resolveInstanceMethod
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是未被找到的selector,在这个方法里,我们可以给目标的类动态添加方法。
类似的
+ (BOOL)resolveClassMethod:(SEL)selector
用来动态添加类方法。
两个方法具体实现下文会做介绍。
forwardingTarget
- (id)forwardingTargetForSelector:(SEL)selector
以上方法可以将消息转发给另外一个接受者。
forwardInvocation
- (void)forwardInvocation:(NSInvocation*)invocation
这是消息转发机制的最后一步,如果转发机制已经来到这一步的话,那我们就需要把尚未处理的消息的全部信息进行重新包装秤一个NSInvocation对象,NSInvocation包含了selector及target参数。触发NSInvocation,消息派发中心会把消息派给NSInvocation中指定的target。
如果到这一步,消息任然无法找到实现,那么就会抛出文章开头所提到的unrecognized selector 错误。
Demo
下面展示一个通过消息转发机制中的resolveInstanceMethod方法来动态添加set和get方法的例子。
在Teacher.h中声明两个属性:
@interface Teacher : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *workNumber;
@end
.m文件中:
#import "Teacher.h"
#import
@interface Teacher()
@property (nonatomic, strong) NSMutableDictionary *propertyDictionary;
@end
@implementation Teacher
@dynamic name,workNumber;
- (instancetype)init
{
if (self = [super init]) {
_propertyDictionary = [[NSMutableDictionary alloc] init];
}
return self;
}
1 propertyDictionary属性的设置是因为没有了原声的Setter和Getter时,我们需要另外一个空间去存储我们的name和workNumber值,最好的方式就是通过字典型变量。
2 dynamic 关键字,表示不需要系统自动生成Setter和Getter,从而可以方便我们进行后面的消息转发。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selectorName = NSStringFromSelector(sel);
if ([selectorName hasPrefix:@"set"]) {
//是Set方法
class_addMethod(self, sel, (IMP)dynmaicSetMethod, "v@:@");
}
else
{
//是get方法
class_addMethod(self, sel, (IMP)dynmaicGetMethod, "@:@");
}
return [super resolveInstanceMethod:sel];
}
以上方法就是具体对resolveInstanceMethod方法的实现。首先用判断语句来判断selector的名称是否包含set,从而判断它是不是一个Setter方法。剩下情况就为Getter方法(实际在方法很多的情况下可能更为复杂)。
方法结尾还是需要调用父类的resolveInstanceMethod方法。
class_addMethod方法
class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
第一个参数为当前需要添加方法的类,基本都写self,第二个为selector,第三个即为你动态添加的方法的具体实现,可能需要加(IMP)进行转换。
最后一个则为函数的类型,用的是C类型的字符串进行描述的(Type Encoding),比如
types参数为"v@:@“,按顺序分别表示:
i:返回值类型为void
@:参数id(self)
::SEL(_cmd)
@:id(str)
具体可以参照官方文档
最后是我们两个Setter和Getter的具体实现:
void dynmaicSetMethod(id self,SEL _cmd,id value)
{
NSString *selectorName = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorName mutableCopy];
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
[key deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *firstCharacter = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter];
NSLog(@"%@",key);
//处理字符串,比如吧setName:处理为name
Teacher *selfTeacher = (Teacher *)self;
if (value) {
[selfTeacher.propertyDictionary setValue:value forKey:key];
}else{
[selfTeacher.propertyDictionary removeObjectForKey:key];
}
}
id dynmaicGetMethod(id self,SEL _cmd)
{
NSString *key = NSStringFromSelector(_cmd);
Teacher *selfTeacher = (Teacher *)self;
return [selfTeacher.propertyDictionary objectForKey:key];
}
具体看代码实现就行。
如果采用这样的写法:
- (id)dynmaicGetMethod{
NSString *key = NSStringFromSelector(_cmd);
Teacher *selfTeacher = (Teacher *)self;
return [selfTeacher.propertyDictionary objectForKey:key];
}
那么在class_addMethod可以写成这样
class_addMethod(self, sel, class_getMethodImplementation(self, @selector(dynmaicGetMethod)), "@:@");
以上就是添加动态方法的实现。
测试:
Teacher *newTeacher = [[Teacher alloc] init];
newTeacher.name = @"hahaha";
newTeacher.workNumber = @"1232435";
NSLog(@"%@",newTeacher.name);
NSLog(@"%@",newTeacher.workNumber);
set和get都可以正常使用。
forwardingTarget
以下举个例子,具体实现还是看具体情况
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(setName:)) {
return [Teacher new];
}
return nil;
}
forwardInvocation
一个简单的实现可以如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
将消息转发给其他对象,但是如果仅仅做一个转发,那么在第二步的forwardTarget就可以做到。
forwardInvocation更像是一个未识别消息的回收和转发站,它可以将消息重新发送给别的对象,也可以简单的吞掉,不做任何处理。
当需要消息重定向的时候,还需要重写methodSignatureForSelector方法。
以下是一个例子:
.h中
@interface TargetProxy : NSProxy {
id realObject1;
id realObject2;
}
- (id)initWithTarget1:(id)t1 target2:(id)t2;
@end
.m中:
@implementation TargetProxy
- (id)initWithTarget1:(id)t1 target2:(id)t2 {
realObject1 = [t1 retain];
realObject2 = [t2 retain];
return self;
}
- (void)dealloc {
[realObject1 release];
[realObject2 release];
[super dealloc];
}
// The compiler knows the types at the call site but unfortunately doesn't
// leave them around for us to use, so we must poke around and find the types
// so that the invocation can be initialized from the stack frame.
// Here, we ask the two real objects, realObject1 first, for their method
// signatures, since we'll be forwarding the message to one or the other
// of them in -forwardInvocation:. If realObject1 returns a non-nil
// method signature, we use that, so in effect it has priority.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig;
sig = [realObject1 methodSignatureForSelector:aSelector];
if (sig) return sig;
sig = [realObject2 methodSignatureForSelector:aSelector];
return sig;
}
// Invoke the invocation on whichever real object had a signature for it.
- (void)forwardInvocation:(NSInvocation *)invocation {
id target = [realObject1 methodSignatureForSelector:[invocation selector]] ? realObject1 : realObject2;
[invocation invokeWithTarget:target];
}
// Override some of NSProxy's implementations to forward them...
- (BOOL)respondsToSelector:(SEL)aSelector {
if ([realObject1 respondsToSelector:aSelector]) return YES;
if ([realObject2 respondsToSelector:aSelector]) return YES;
return NO;
}
@end
测试代码:
id proxy = [[TargetProxy alloc] initWithTarget1:string target2:array];
// Note that we can't use appendFormat:, because vararg methods
// cannot be forwarded!
[proxy appendString:@"This "];
[proxy appendString:@"is "];
[proxy addObject:string];
[proxy appendString:@"a "];
[proxy appendString:@"test!"];
NSLog(@"count should be 1, it is: %d", [proxy count]);
if ([[proxy objectAtIndex:0] isEqualToString:@"This is a test!"]) {
NSLog(@"Appending successful.");
} else {
NSLog(@"Appending failed, got: '%@'", proxy);
}
运行的结果是:
count should be 1, it is: 1
Appending successful.
TargetProxy声明中是没有appendString与addObject消息的,在这儿却可以正常发送,不crash,原因就是发送消息的时候,如果原本类没有这个消息响应的时候,转向询问methodSignatureForSelector,接着在forwardInvocation将消息重定向。
总结
1 若对象无法响应某个选择子,则进入消息转发流程。
2通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
3 对象可以把其无法解读的某些选择子转交给其他对象来处理。
4 如果上述步骤仍然无法处理消息,则对消息的目标,selector进行重新包装转发。