高效编写代码的方法(十):消息转发机制

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最后一次识别机会。

以上就是一个完整的消息转发机制。

消息转发机制

高效编写代码的方法(十):消息转发机制_第1张图片
示意图

上图是一个消息转发机制的流程。下面详解。

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进行重新包装转发。

你可能感兴趣的:(高效编写代码的方法(十):消息转发机制)