OC中的消息转发机制
在本文中,将为你解释在OC的动态机制中,一个对象是如何调用,并且在对象中找不到方法的情况下,如何将方法通过"发消息"的形式转发给其它接受者。
在了解消息转发机制之间,要知道OC中方法如何调用,并且为什么需要消息转发。
OC的语法中,我们调用这样调用一个方法:
[receiver method:param];
编译后的c代码是这样调用方法的:
objc_msgSend(receiver, @selector(method), param);
- 在OC的所有方法中,其实有两个隐藏参数,一个是id receiver规定了方法的接受者,另一个是SEL _cmd,方法的selector。
究竟在msgSend方法中做了什么?
在runtime源码中,msgSend方法在objc-msg-arm.s中,为了保证效率,使用了汇编实现。
id objc_msgSend(id self, SEL _cmd,...);
ENTRY objc_msgSend
MESSENGER_START
cbz r0, LNilReceiver_f ;receiver为空则直接返回
ldr r9, [r0] ;r9 = self->isa
CacheLookup NORMAL ; calls IMP or LCacheMiss
; 首先在缓存中寻找IMP
LCacheMiss: ; 缓存中找不到IMP
MESSENGER_END_SLOW
ldr r9, [r0, #ISA] ; class = receiver->isa
b __objc_msgSend_uncached ;在父类中寻找IMP
LNilReceiver: ; 找不到IMP,标识IMP为消息转发
; r0 is already zero
mov r1, #0
mov r2, #0
mov r3, #0
FP_RETURN_ZERO
MESSENGER_END_NIL
bx lr
LMsgSendExit:
END_ENTRY objc_msgSend
在源码中有清晰的注释,让我们知道,msgSend方法实际上逻辑不复杂。
- 第一步,判断receiver是否为空,若为空则直接返回,这就是给一个空对象调用方法不会奔溃的原因。
- 第二步,在缓存中搜索是否已经缓存IMP,若已缓存,则直接返回。
- 第三步,在父类中寻找IMP方法,找到则返回。* 第四步,标记IMP为_objc_msgForward,意思是调用这个方法直接走消息转发机制。
在类中寻找IMP
在上面的描述中,我们需要在一个类中寻找方法的实现,这个寻找方法,在runtime源码objc-class-old.mm中的方法:
IMP lookUpImpOrForward(Class cls,
SEL sel,
id inst,
bool initialize,
bool cache,
bool resolver)
主要执行了以下动作
- 解锁methodList,用于无锁查找,速度快。
- 在缓存中查找,如果找到,则返回缓存的方法。
- 查找cls是否被释放,如果是,则返回cls被释放的错误。
- 判断cls是否已经初始化,没有初始化则初始化。
- 加锁methodList,防止多线程操作。
- 如果是垃圾回收机制的方法,则忽略,并添加到类的忽略方法列表中。
- 尝试查找缓存,找到则返回。
- 尝试在这个类的方法列表methodList中查找,如果找到,则添加到缓存中。
- 如果还没有找到,就从父类的缓存和父类的方法列表中查找,找到就添加到缓存中,没找到就进入_class_resolveMethod方法。
- 调用_class_resolveMethod方法,给机会动态添加方法,然后重新调用msgSend查找,看看有没有添加方法。
- 将这个方法直接缓存为消息转发。
- 解锁methodList。
到此,已经和msgSend没什么关系了,关键在于msgSend方法中,找不到方法的实现,则开始消息转发。
消息转发
上面我们提到,当实在找不到方法实现时,会走消息转发。你有3次机会添加方法的实现,如果还没有方法的实现,程序只能Crash了。接下来用例子分别说明这3次机会。新建一个Son和Father类,并在Son类中添加方法声明,不实现方法。
// Son.h
#import <Foundation/Foundation.h>
@interface Son : NSObject
- (void)method;
@end
// Son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
@end
然后,调用son的method方法。
son调用method奔溃.png
结果可想而知,son的method方法没有实现,并且也没有在消息转发过程中添加实现,所以出现经典的奔溃:
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[Son method]: unrecognized selector sent to
instance 0x1003031e0'
那在消息转发中,调用了哪些方法呢?我们给[son method]
方法打上断点,并在gbd中输入命令call (void)instrumentObjcMessageSends(YES)
。
然后在终端中打开文件夹
open /private/tmp
找到"msgSend-xxxx"之类的文件,双击打开。消息转发方法.png
这些就是在消息转发的过程中调用的方法。
1.resolveInstanceMethod: (或 resolveClassMethod:)方法。这是第一次机会让你添加方法实现。在这里,调用class_addMethod方法添加实现,并返回YES,然后会重新开始msgSend流程。如果没有实现,那么进入第二次机会。
2.forwardingTargetForSelector:方法。这是第二次机会,但是这一次不是添加方法实现,而是将消息转发给能够响应此消息的对象,直接把消息发给它。否则返回nil。
3.methodSignatureForSelector:方法。这是第三次机会,这个方法尝试获取方法的签名如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。刚刚奔溃的
unrecognized selector sent to instance
错误就出自于此。 如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。4.forwardInvocation:方法。将第三步的方法签名添加一个方法实现。
5.doesNotRecognizeSelector: 方法。抛出找不到方法签名的移除,程序Crash。###知道的消息转发的怎么回事,怎么去使用呢?
第一次机会:
+(BOOL)resolveInstanceMethod:(SEL)sel;
在son.m中重写resolveInstanceMethod:方法,使用class_addMethod()增加method方法的实现:
// son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
void method(id self, SEL _cmd){
NSLog(@"son method");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(method)) {
class_addMethod(self, sel, (IMP)method, "v@:" );
return YES;
}
return [super resolveInstanceMethod:sel];}@end
可以看到已经成功执行method方法:
第一次机会实现son method.png第二次机会:
-(id)forwardingTargetForSelector:(SEL)aSelector;
在son.m中重写forwardingTargetForSelector:方法,返回能相应method方法的实例,更换消息的接收者。下面就是让father实例接收method方法:
// son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
-(id)forwardingTargetForSelector:(SEL)aSelector{
return [[Father alloc] init];
}
@end
然后在father.m中实现method方法:
// father.m
#import "Father.h"
@implementation Father
-(void)method{
NSLog(@"Father method");
}
end
惊讶地发现,son实例调用method方法,却是father实现去相应method方法:
第二次机会实现son method.png
第三次机会:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
-(void)forwardInvocation:(NSInvocation *)anInvocation
在son.m中,重写-(NSMethodSignature *)methodSignatureForSelector方法,返回一个method
方法的签名,并在forwardInvocation:方法中,指定方法的响应者:
// Son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *sel = NSStringFromSelector(aSelector);
if([sel isEqualToString:@"method"]){
//手动为method方法添加签名
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
SEL selector = [anInvocation selector]; //新建需要接受消息的对象
Father *father = [[Father alloc] init];
if([father respondsToSelector:selector]){
//接收对象唤醒方法
[anInvocation invokeWithTarget:father];
}
}
@end
最终的结果还是father响应了method方法:
第三次机会实现son method.png那消息转发有什么用?
在日常开发中,使用消息转发的场景并不多,大多数是防止程序Crash。当然,你也可以直接调用_objc_msgForward
,这样会告诉msgSend方法,直接进入消息转发。著名的热修复工具JSPath,就是直接调用_objc_msgForward
实现的。
OC中的消息转发大致如此,有什么问题可以留言,我们共同探讨哦。