Objective-C 的消息转发

3,183 阅读10分钟

1 方法与消息

1.1 方法与消息发送

消息在OC中方法调用是一个消息发送的过程。OC方法最终被生成为C函数,并带有一些额外的参数。这个C函数objc_msgSend就负责消息发送。在<objc/message.h>中能找到它的API。

objc_msgSend(void /* id self, SEL op, ... */ )

1.2 消息转发的步骤:

  1. 检测这个 selector 的 target 是不是nil,OC允许我们对一个nil对象执行任何方法不会崩溃,因为运行时会被忽略掉。

  2. 找这个类的实现(IMP),先从 cache (objc_cache)里查找,如果找到了就运行对应的函数去执行相应的代码。

  3. 如果cache中没有找到就找类的方法列表中是否有对应的方法。如果类的方法列表中找不到就到父类的方法列表中查找,一直找到NSObject类为止。

  4. 如果还是没找到就要开始进入动态方法解析和消息转发,涉及到的方法有如下三组:

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

- (id)forwardingTargetForSelector:(SEL)aSelector;

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

1.3 OC的方法本质

OC中的方法默认被隐藏了两个参数:self_cmd。 在编译时你写的 OC 函数调用的语法都会被翻译成一个 C 的函数调用 objc_msgSend()。比如,下面两行代码就是等价的:

[array insertObject:foo atIndex:5];
//等价于
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

2 动态特性:方法解析和消息转发

没有方法的实现,程序会在运行时崩溃并抛出 unrecognized selector sent to... 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:

  • Method resolution
  • Fast forwarding
  • Normal forwarding

2.1 动态方法解析: Method Resolution

首先,Objective-C 运行时会调用 + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程

#import<Foundation/Foundation.h>
#import<objc/runtime.h>
@interface MyClass: NSObject
+ (void)badClassMethod;
- (void)badInstanceMethod;
@end

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s sel: %s", __func__, sel_getName(sel));
    if (sel == @selector(badInstanceMethod)) {
            class_addMethod(self, sel, (IMP)surviveBadInstanceMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s sel: %s", __func__, sel_getName(sel));
    if (sel == @selector(badClassMethod)) {
        class_addMethod(objc_getMetaClass(object_getClassName(self)), sel, (IMP)surviveBadClassMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

void surviveBadClassMethod(id self, SEL _cmd) {
    //NSLog(@"%s: %@ %s", __func__, self, sel_getName(_cmd));
    NSLog(@"%s", __func__);
}

void surviveBadInstanceMethod(id self, SEL _cmd) {
    //NSLog(@"%s: %@ %s", __func__, self, sel_getName(_cmd));
    NSLog(@"%s", __func__);
}

@end

int main(int argc, char *argv[]) {
    MyClass *obj = [MyClass new];
    [MyClass badClassMethod];
    [obj badInstanceMethod];
    return 0;
}

输出

+[MyClass resolveClassMethod:] sel: badClassMethod
surviveBadClassMethod
+[MyClass resolveInstanceMethod:] sel: badInstanceMethod
surviveBadInstanceMethod

题外话:可以尝试在上面的surviveBadClassMethodsurviveBadInstanceMethod中打开注释的内容,会发现多打印了几行内容。因为NSlogself,会调用± (NSString *)description方法。

需要注意的是类方法+ (void)badClassMethod的解析,动态增加方法时候,需要加到元类上,因为类方法列表是在元类对象中存储的。

/** 
 * Adds a new method to a class with a given name and implementation.
 * 
 * @param cls The class to which to add a method.
 * @param name A selector that specifies the name of the method being added.
 * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
 * @param types An array of characters that describe the types of the arguments to the method. 
 * 
 * @return YES if the method was added successfully, otherwise NO 
 *  (for example, the class already contains a method implementation with that name).
 *
 * @note class_addMethod will add an override of a superclass's implementation, 
 *  but will not replace an existing implementation in this class. 
 *  To change an existing implementation, use method_setImplementation.
 */
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);

这里第一字符v代表函数返回类型void,第二个字符@代表self的类型id,第三个字符:代表_cmd的类型SEL。这些符号可在Xcode中的开发者文档中搜索Type Encodings就可看到符号对应的含义,详见官方文档

2.2 快速转发

Fast Rorwarding只需要在指定 API 方法里面返回一个新对象即可。通过- (id)forwardingTargetForSelector:(SEL)aSelector方法,运行时系统允许我们替换消息的接收者为其他对象。如果此方法返回的是nil或者self,则会进入消息转发机制- (void)forwardInvocation:(NSInvocation *)invocation,否则将会向返回的对象重新发送消息。

#import<Foundation/Foundation.h>

@interface YourClass: NSObject
@end

@implementation YourClass
- (void)badInstanceMethod {
    NSLog(@"Gotcha!");
}
@end

@interface MyClass: NSObject
+ (void)badClassMethod;
- (void)badInstanceMethod;
@end

@implementation MyClass
- (id)forwardingTargetForSelector:(SEL)sel {
    NSLog(@"%s: %@ %s", __func__, self, sel_getName(_cmd));
    if (sel ==  @selector(badInstanceMethod)) {
        return [[YourClass alloc] init];
    }
    return [super forwardingTargetForSelector:sel];
}
@end

int main(int argc, char *argv[]) {
    MyClass *obj = [MyClass new];
    [obj badInstanceMethod];
    return 0;
}

2.3 完整消息转发: Normal Forwarding

先看代码。

#import<Foundation/Foundation.h>

@interface YourClass: NSObject
@end

@implementation YourClass
- (void)badInstanceMethod {
    NSLog(@"Gotcha!");
}
@end

@interface TheirClass: NSObject
@end

@implementation TheirClass
- (void)badInstanceMethod {
    NSLog(@"Fuck off!");
}
@end

@interface MyClass: NSObject
- (void)badInstanceMethod;
@end

@implementation MyClass
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    YourClass *yourObj = [YourClass new];
    TheirClass *theirObj = [TheirClass new];
    if([yourObj respondsToSelector:sel]) {
        [invocation invokeWithTarget:yourObj];
    }
    if ([theirObj respondsToSelector:sel]) {
        [invocation invokeWithTarget:theirObj];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return methodSignature;
}
@end

int main(int argc, char *argv[]) {
    MyClass *obj = [MyClass new];
    [obj badInstanceMethod];
    return 0;
}

-forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。

其中,参数invocation是从哪来的?在-forwardInvocation:消息发送前,runtime 会向对象发送-methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写-forwardInvocation:的同时也要重写-methodSignatureForSelector:方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,runtime 将通过-forwardInvocation:消息通知该对象。Normal Forwarding 可以连续转发,Fast Forwarding 则不行。

总结

Objective-C 中给一个对象发送消息会经过以下几个步骤:

在对象类的 dispatch table (?)中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;

如果没有找到,运行时系统会发送+resolveInstanceMethod:或者 +resolveClassMethod:尝试去 resolve 这个消息;

如果 resolve 方法返回NO,运行时系统就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;

如果没有新的目标对象返回,运行时系统就会发送 -methodSignatureForSelector:-forwardInvocation: 消息。你可以发送-invokeWithTarget:消息来手动转发消息或者发送 -doesNotRecognizeSelector:抛出异常。

文末附录是相关API的文档,强烈建议阅读!

附录:相关API

forwardInvocation:

Implementations of the forwardInvocation: method can do more than just forward messages. forwardInvocation: can, for example, be used to consolidate code that responds to a variety of different messages, thus avoiding the necessity of having to write a separate method for each selector. A forwardInvocation: method might also involve several other objects in the response to a given message, rather than forward it to just one.

NSObject’s implementation of forwardInvocation: simply invokes the doesNotRecognizeSelector: method; it doesn’t forward any messages. Thus, if you choose not to implement forwardInvocation:, sending unrecognized messages to objects will raise exceptions.

forwardInvocation:方法的实现可以做的不仅仅是转发消息。例如,forwardInvocation:可用于合并响应各种不同消息的代码,从而避免必须为每个选择器编写单独的方法。forwardInvocation:方法也可以在消息响应时包含其他对象,而不是只将其转发给一个消息。

NSObject的forwardInvocation:的实现只是调用doesNotRecognizeSelector:方法,并不转发任何消息。因此,如果你选择不实现forwardInvocation:,则向对象发送无法识别的消息将引发异常。

NSInvocation

NSInvocation objects are used to store and forward messages between objects and between applications, primarily by NSTimer objects and the distributed objects system. An NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value. Each of these elements can be set directly, and the return value is set automatically when the NSInvocation object is dispatched.

NSInvocation对象主要用于NSTimer对象和分布式对象系统,用于在对象以及应用程序之间之间存储和转发消息。 NSInvocation对象包含Objective-C消息的所有元素:目标,选择器,参数和返回值。每个元素都可以直接设置,并在调度NSInvocation对象时自动设置返回值。

An NSInvocation object can be repeatedly dispatched to different targets; its arguments can be modified between dispatch for varying results; even its selector can be changed to another with the same method signature (argument and return types). This flexibility makes NSInvocation useful for repeating messages with many arguments and variations; rather than retyping a slightly different expression for each message, you modify the NSInvocation object as needed each time before dispatching it to a new target.

可以将NSInvocation对象重复分派给不同的目标;它的参数可以在发送之间修改以获得不同的结果;甚至可以使用相同的方法签名(参数和返回类型)将其选择器更改为另一个。这种灵活性使NSInvocation对于重复具有许多参数和变体的消息非常有用;而不是为每条消息重新输入略有不同的表达式,每次根据需要修改NSInvocation对象,然后再将其分配给新目标。

NSInvocation does not support invocations of methods with either variable numbers of arguments or union arguments. You should use the invocationWithMethodSignature: class method to create NSInvocation objects; you should not create these objects using alloc and init.

NSInvocation不支持使用可变数量的参数或联合参数调用方法。您应该使用invocationWithMethodSignature:类方法来创建NSInvocation对象;你不应该使用allocinit创建这些对象。

This class does not retain the arguments for the contained invocation by default. If those objects might disappear between the time you create your instance of NSInvocation and the time you use it, you should explicitly retain the objects yourself or invoke the retainArguments method to have the invocation object retain them itself.

默认情况下,此类不持有包含的调用的参数。如果这些对象可能在你创建NSInvocation实例和使用它的时间之间消失,那么你应该自己显式持有这些对象,或者调用retainArguments方法让调用对象自己持有它们。

NSMethodSignature

Overview

Use an NSMethodSignature object to forward messages that the receiving object does not respond to—most notably in the case of distributed objects. You typically create an NSMethodSignature object using the NSObject methodSignatureForSelector: instance method (in macOS 10.5 and later you can also use signatureWithObjCTypes:). It is then used to create an NSInvocation object, which is passed as the argument to a forwardInvocation: message to send the invocation on to whatever other object can handle the message. In the default case, NSObject invokes doesNotRecognizeSelector:, which raises an exception. For distributed objects, the NSInvocation object is encoded using the information in the NSMethodSignature object and sent to the real object represented by the receiver of the message.

使用NSMethodSignature对象转发接收对象不响应的消息—— 尤其是在分布式对象(译注:这个概念好像不太常见,文档见Distributed Objects Architecture )的情况下。通常使用NSObject methformSignatureForSelector:实例方法创建一个NSMethodSignature对象(在macOS 10.5及更高版本中,也可以使用signatureWithObjCTypes:)。然后,它用于创建NSInvocation对象,该对象作为参数传递给forwardInvocation:消息,以将 invocation 发送到其他可以处理消息的对象。在默认情况下,NSObject调用doesNotRecognizeSelector:,引发异常。对于分布式对象,使用NSMethodSignature对象中的信息对NSInvocation对象进行编码,并将其发送到由消息接收者表示的真实对象。

Type Encodings

An NSMethodSignature object is initialized with an array of characters representing the string encoding of return and argument types for a method. You can get the string encoding of a particular type using the @encode() compiler directive. Because string encodings are implementation-specific, you should not hard-code these values.

NSMethodSignature对象用字符数组(方法的返回值和参数类型的字符串编码)来初始化。可以用@encode()编译器指令得到特定类型的字符串编码。由于字符串编码与特定实现相关的,不应在代码中写死。

A method signature consists of one or more characters for the method return type, followed by the string encodings of the implicit arguments self and _cmd, followed by zero or more explicit arguments. You can determine the string encoding and the length of a return type using methodReturnType and methodReturnLength properties. You can access arguments individually using the getArgumentTypeAtIndex: method and numberOfArguments property.

方法签名包含一个或多个字符,顺序是返回值,隐式参数self_cmd的字符串编码,以及零个或多个显式参数。你可以用methodReturnTypemethodReturnLength属性确定类型的字符串编码和返回类型长度。你可以用getArgumentTypeAtIndex:方法和numberOfArguments属性分别获取每个参数。

#import <Foundation/Foundation.h>

int main(int argc, char *argv[]) {
	@autoreleasepool {
		NSMutableString *str = [@"Just for fun!" mutableCopy];
		NSMethodSignature *ms = [str methodSignatureForSelector:@selector(stringByReplacingCharactersInRange:withString:)];
		NSLog(@"return type:%s, number of bytes:%lu", [ms methodReturnType], [ms methodReturnLength]);
		NSUInteger argsCount = [ms numberOfArguments];
		NSLog(@"number of arguments:%lu", argsCount);
		for(int i = 0; i < argsCount; i++) {
			NSLog(@"type of argument #%d:%s", i, [ms getArgumentTypeAtIndex:i]);
		}
	}
}

输出:

 return type:@, number of bytes:8
 number of arguments:4
 type of argument #0:@
 type of argument #1::
 type of argument #2:{_NSRange=QQ}
 type of argument #3:@

参考文献:iOS开发·runtime原理与实践: 消息转发篇

扩展阅读: 扩展阅读:

  1. iOS保护应用安全,拒绝forwardInvocationdemo
  2. 深入理解Objective-C:方法缓存
  3. Runtime源码解析
  4. 从源代码看 ObjC 中消息的发送
  5. 如何正确使用 Runtime