iOS Runtime-动态消息解析和消息转发机制

动态消息解析

你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:

1
@dynamic propertyName;

这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:和propertyName方法,而需要我们自己实现或者动态提供。

我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。

我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:

1
2
3
4
5
6
7
8
9
10
11
12
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}

上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数.

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector: 或 instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO。

实例方法和类方法的解析对比

.h文件

1
2
3
4
5
6
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}
- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end

需要深刻理解 [self class] 与 object_getClass(self) 甚至 object_getClass([self class]) 的关系,其实并不难,重点在于 self 的类型:

  • 当 self 为实例对象时,[self class] 与 object_getClass(self) 等价,因为前者会调用后者。object_getClass([self class]) 得到元类。

  • 当 self 为类对象时,[self class] 返回值为自身,还是 self。object_getClass(self) 与 object_getClass([self class]) 等价。

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:

1
2
3
4
5
6
7
8
9
- (id)forwardingTargetForSelector:(SEL)aSelector
{
//如果代理对象能处理,则转接给代理对象
if ([proxyObj respondsToSelector:aSelector]) {
return proxyObj;
}
//不能处理进入转发流程
return nil;
}

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择。 如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

转发

当动态方法解析不作处理返回nil时,消息转发机制会被触发。

这时下面两个方法需要被重写:

1
2
3
4
5
//获取该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
//手动调用该方法
- (void)forwardInvocation:(NSInvocation *)anInvocation

ps: 如果 methodSignatureForSelector 返回的NSMethodSignature 是 nil 的话不会继续执行 forwardInvocation,转发流程终止,抛出无法处理的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *sig = [BBMessageForwardProxy instanceMethodSignatureForSelector:@selector(bb_dealNotRecognizedMessage:)];
return sig;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSString *debugInfo = [NSString stringWithFormat:@"[debug]unRecognizedMessage:[%@] sent to [%@]",NSStringFromSelector(anInvocation.selector),NSStringFromClass([self class])];
//重定向方法
[anInvocation setSelector:@selector(bb_dealNotRecognizedMessage:)];
//传递调用信息
[anInvocation setArgument:&debugInfo atIndex:2];
//BBMessageForwardProxy对象接收转发的消息并打印调用信息
[anInvocation invokeWithTarget:[BBMessageForwardProxy new]];
}

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

ps:forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

至此 消息的整个转发过程结束。