iOS中消息转发的套路

3,032 阅读7分钟

消息转发

消息转发是Objective-C的消息机制的一个特性而已,实际工程中使用到的场景其实不多,不过了解其机制也是很有必要的。 OC的消息机制,允许用户在未实现某个消息(SEL)的具体方法(IMP)时,依然有机会能够响应该消息。可以理解为是发送消息的一个补充,专用于处理未找到消息的情况。 所以,普通的发送消息,如果没有在实例对象的类(或者类对象的元类)的方法列表中找到对应的方法,则会进入消息转发的流程,包含以下几个步骤。

动态方法解析

resolveInstanceMethod:与resolveClassMethod:

这两个方法,允许开发者动态添加方法的具体实现。

static void sayInstanceName(id self, SEL cmd, id value) {
    NSLog(@"resolveInstanceMethod %@", value);
}

static void sayClassName(id self, SEL cmd, id value) {
    NSLog(@"resolveClassMethod %@", value);
}
@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod");
    
    NSString *methodName = NSStringFromSelector(sel);
    if ([methodName isEqualToString:@"sayInstanceName:"]) {
        class_addMethod([self class], sel, (IMP)sayInstanceName, "v@:@");
        return YES;
    } else if ([methodName isEqualToString:@"dynamicName"]) {
        class_addMethod(self, sel, (IMP)myDynamicName, "@@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

// 注意:
// self为实例对象时,[self class]与object_getClass(self)都返回类(前者调用后者),object_getClass([self class])返回元类。
// self为类对象时,[self class]返回自身,而object_getClass(self)返回元类,等价于object_getClass([self class])。
+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSString *methodName = NSStringFromSelector(sel);
    if ([methodName isEqualToString:@"sayClassName:"]) {
        class_addMethod(object_getClass(self), sel, (IMP)sayClassName, "v@:@");
        return YES;
    }
    
    return [super resolveClassMethod:sel];
}

@end

可以设置实例方法和类方法。

注意,这里会涉及到方法实现的Type Encoding,我们暂时先了解简单的对应关系即可。

[返回值][target][action][参数]

// 如 v@:@, 即

// v void
// @ id
// : SEL
// @ id

forwardingTargetForSelector

消息转发,可以将消息转发给可以响应该消息的一个对象,这个对象可以是实例对象的一个属性,也可以是毫不相关的另外一个对象。

// 仅支持将消息转发给一个对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"forwardingTargetForSelector");
 
    // 此处可以询问对象的所有属性,看有谁可以响应消息,即将其return
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sayInstanceName:"]) {
        if ([self.helper respondsToSelector:aSelector]) {
            return self.helper;
        }
        
        // 并不会执行,所以forwardingTargetForSelector仅支持将消息转发给一个对象
        if ([self.anotherHelper respondsToSelector:aSelector]) {
            return self.anotherHelper;
        }
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

注意:这里只能将消息转发给 一个对象

forwardInvocation

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"methodSignatureForSelector");
 
    if ([self.helper respondsToSelector:aSelector]) {
        return [self.helper methodSignatureForSelector:aSelector];
    }
    
    if ([self.anotherHelper respondsToSelector:aSelector]) {
        return [self.anotherHelper methodSignatureForSelector:aSelector];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

 
// 支持将消息转发给任意多个对象,所以多继承也只能采用forwardInvocation:的方式
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"forwardInvocation");
 
    SEL sel = anInvocation.selector;
    
    if ([self.helper respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self.helper];
    }
    
    if ([self.anotherHelper respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self.anotherHelper];
    }
}

这里,对方法的签名进行严格匹配,然后再执行对应的消息转发。

doesNotRecognizeSelector

在以上步骤全部执行过,依然没能完成发送消息,则会调用如下方法:

// unrecognized selector sent to instance
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
    NSLog(@"doesNotRecognizeSelector");
}

NSObject对该方法的默认实现即抛出一个异常,如:

[Person sayClassName:]: unrecognized selector sent to class 0x109344950

forwardSelector与NSInvocation的区别

通常情况下,这两个阶段的区别如下:

  • forwardSelector只能将消息尝试转发给一个对象。
  • 而forwardInvocation则可以将消息转发给多个对象。

深入理解NSInvocation

NSMethodSignature

方法签名包含了方法的名称、参数、返回值。iOS中使用Type Encoding的方式来表示。

  1. target
  2. selector
  3. arguments
  4. return value

例如下边的方法:

- (void)myVoid:(NSDictionary *)params {
    NSLog(@"myFunc %@", params);
}

- (BOOL)myBool:(NSDictionary *)params {
    NSLog(@"myFunc %@", params);
    return YES;
}

- (NSInteger)myNSInteger:(NSDictionary *)params {
    NSLog(@"myFunc %@", params);
    return 1;
}

- (CGFloat)myCGFloat:(NSDictionary *)params {
    NSLog(@"myFunc %@", params);
    return CGFLOAT_MAX;
}

- (id)myFunc:(NSDictionary *)params {
    NSLog(@"myFunc %@", params);
    return [params copy];
}
- (void)testInvocations {
    SEL myVoid = @selector(myVoid:);
    SEL myBool = @selector(myBool:);
    SEL myNSInteger = @selector(myNSInteger:);
    SEL myCGFloat = @selector(myCGFloat:);
    SEL myFunc = @selector(myFunc:);
    NSDictionary *params = @{@"name": @"name"};
    NSMethodSignature *myVoidSig = [self methodSignatureForSelector:myVoid]; // v@:@
    NSMethodSignature *sigMyBool = [self methodSignatureForSelector:myBool]; // B@:@
    NSMethodSignature *sigMyNSInteger = [self methodSignatureForSelector:myNSInteger]; // q@:@
    NSMethodSignature *sigMyCGFloat = [self methodSignatureForSelector:myCGFloat]; // d@:@
    NSMethodSignature *sigMyFunc = [self methodSignatureForSelector:myFunc]; // @@:@
    NSLog(@"testInvocations");
}

重复一次,Type Encoding的格式为:

[返回值][target][action][参数]

各个类型对应的表示分别为 void-v,Bool-B,NSInteger-q,CGFlat-d,id-@ 。

以myFunc:为例,表示为 @@:@ ,即返回值为id,接收参数也为NSObject类型(NSDictonary)。

NSInvocation

NSInvocation可以给任意OC对象发送消息,其使用方式有固定的步骤:

  1. 根据selector来初始化方法签名对象NSMethodSignature
  2. 根据方法签名对象NSMethodSignature来初始化NSInvocation对象,必须使用invocationWithMethodSignature:方法。
  3. 设置target和selector。
  4. 设置参数,注意参数的index从2开始,因为0和1分别对应为target和selector。若参数index超出则会出错。
  5. 调用NSInvocation对象的invoke方法。
  6. 若有返回值,使用NSInvocation对象的getReturnValue来获取返回值。
- (void)testInvocation {
    SEL sel = @selector(myFunc:);
    NSDictionary *params = @{@"name": @"name"};
    NSMethodSignature *sig = [self methodSignatureForSelector:sel];
    if (!sig) {
        return;
    }
    
    const char *retType = [sig methodReturnType];
    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
        invocation.target = self;
        invocation.selector = sel;
        [invocation setArgument:&params atIndex:2];
        [invocation invoke];
        NSLog(@"void ret");
        
        /// 0是target,1是action。参数是从2开始的。
        void *target;
        SEL action;
        [invocation getArgument:&target atIndex:0];
        [invocation getArgument:&action atIndex:1];
        /// target-action : <AppDelegate: 0x600003d69080>-myFunc:
        NSLog(@"target-action : %@-%@", target, NSStringFromSelector(action));
    } else if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
        invocation.target = self;
        invocation.selector = sel;
        [invocation setArgument:&params atIndex:2];
        [invocation invoke];
        NSInteger ret = 0;
        [invocation getReturnValue:&ret];
        NSLog(@"NSInteger ret %ld", (long)ret);
    } else if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
        invocation.target = self;
        invocation.selector = sel;
        [invocation setArgument:&params atIndex:2];
        [invocation invoke];
        NSUInteger ret = 0;
        [invocation getReturnValue:&ret];
        NSLog(@"NSUInteger ret %ld", (long)ret);
    } else if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
        invocation.target = self;
        invocation.selector = sel;
        [invocation setArgument:&params atIndex:2];
        [invocation invoke];
        BOOL ret = false;
        [invocation getReturnValue:&ret];
        NSLog(@"BOOL ret %ld", (long)ret);
    } else if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
        invocation.target = self;
        invocation.selector = sel;
        [invocation setArgument:&params atIndex:2];
        [invocation invoke];
        CGFloat ret = 0;
        [invocation getReturnValue:&ret];
        NSLog(@"CGFloat ret %ld", (long)ret);
    } else {
        /// performSelector比较严格,如果返回值不匹配,则很可能会导致crash。
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        id ret = [self performSelector:sel withObject:params];
        #pragma clang diagnostic pop
        NSLog(@"id ret %@", ret);
    }
}

retainArguments

注意,这里NSInvocation默认不会强引用其各个参数,所以若参数在NSInvocation执行前就被释放则会造成野指针异常(EXC_BAD_ACCESS)。

如果有需要强引用参数的场景,如延迟执行invoke方法,则需要对参数进行强持有操作。调用retainArguments即可,同时有一个属性argumentsRetained可以用来判断。

请看开发者文档的详细描述,注意其中的细节:对象类型是retain操作,而C-string和blocks则是copy操作。

Instance Method
retainArguments
If the receiver hasn’t already done so, retains the target and all object arguments of the receiver and copies all of its C-string arguments and blocks. If a returnvalue has been set, this is also retained or copied.

Declaration
- (void)retainArguments;

Discussion
Before this method is invoked, argumentsRetained returns NO; after, it returns YES.

For efficiency, newly created NSInvocation objects don’t retain or copy their arguments, nor do they retain their targets, copy C strings, or copy any associated blocks. You should instruct an NSInvocation object to retain its arguments if you intend to cache it, because the arguments may otherwise be released before the invocation is invoked. NSTimer objects always instruct their invocations to retain their arguments, for example, because there’s usually a delay before a timer fires.

getReturnValue

getReturnValue方法,仅仅将返回数据拷贝到指定内存区域,并不考虑内存管理。若返回对象类型,则为__unsafe_unretained。优化办法有:

手动添加一个强持有,则返回对象会自动添加autorelease关键字,不会出现野指针异常。

NSObject __unsafe_unretained *tmpRet;
[invoke getReturnValue:&tmpRet];
NSObject *ret = tmpRet;
return ret;

或者,使用__bridge进行类型转换,这种做法更为推荐。

void *tmpRet = NULL;
[invoke getReturnValue:&tmpRet];
NSObject *ret = (__bridge NSObject *)tmpRet;
return ret;

消息转发的使用场景

动态属性

如果对属性使用了@dynamic关键字,则编译器不会自动为其生成getter/setter方法,而是通过动态查找的方法。

所以,如果使用了@dynamic关键字,而没有手动添加getter/setter方法,则使用时会出错。

@property (nonatomic, copy) NSString *dynamicName;
...

@implementation Person

@dynamic dynamicName;

...
@end

可以通过resolveInstanceMethod:方法动态添加getter/setter即可。

多继承

OC的面向对象是单一继承的关系。如果有个别场景需要用到多继承,可以是要forwardInvocation:来实现。不过不到万不得已,最好不要这样做。

// 用于描述被转发的消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"methodSignatureForSelector");
    
    id p1 = [[NSClassFromString(@"BasePerson1") alloc] init];
    if ([p1 respondsToSelector:aSelector]) {
        return [p1 methodSignatureForSelector:aSelector];
    }
    
    id p2 = [[NSClassFromString(@"BasePerson2") alloc] init];
    if ([p2 respondsToSelector:aSelector]) {
        return [p2 methodSignatureForSelector:aSelector];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"forwardInvocation");
    
    SEL sel = anInvocation.selector;
    
    id p1 = [[NSClassFromString(@"BasePerson1") alloc] init];
    if ([p1 respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p1];
    }
    
    id p2 = [[NSClassFromString(@"BasePerson2") alloc] init];
    if ([p2 respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p2];
    }
}

参考资料