一、消息发送流程
在iOS objc_msgSend 流程和iOS objc_msgSend 慢速查找流程分析中我们分析了对象接收到消息后 objc_msgSend
首先会在对象类的缓存 cache
, 方法列表 method list
以及父类对象的缓存 cache
, 方法列表 method list
依次查找 选择子 SEL
对应的 函数指针 IMP
。
如果没有找到,由于OC是动态语言,所以在运行时还可以继续向类中添加方法,所以当对象收到无法解读的消息时,就会启动 消息转发机制
,我们可以经由这个机制告诉程序该怎么处理这种消息。
消息转发
会分成两大阶段,第一阶段叫做动态方法解析
dynamic method resolution
:先征询当前接收者所属的类,是否能动态添加方法并处理这个未知的 selector
,如果接收者没有动态添加方法或者动态添加的方法依然不能处理这个未知的 selector
,则当前接收者自己就没有办法通过动态新增方法的手段来响应这个 selector
了,之后就进入消息转发的第二阶段。第二阶段可以分成两步,第一步接收者会查看是否存在其他对象能处理这条消息,如果有,则这个处理消息的对象叫备援接收者 replacement receiver
,runtime
系统会把消息转发给这个对象,消息转发流程结束。第二步,如果连备援接收者都没有,则启动完整的消息转发,runtime
系统会把和消息有关的所有信息都放进 NSInvocation
对象中,再给接收者一次机会,处理未知的 selector
,如果这一步都失败了,就会抛出 unrecognize selector send to instance xxx
这个异常。
但在抛出 unrecognized selector sent to … 异常之前,Objective-C
运行时会给你三次拯救程序的机会:
- 动态方法解析
Method resolution
- 快速转发
Fast forwarding
- 完整消息转发
Normal forwarding
二、动态方法决议 Method resolution
对象在收到无法解读的消息后,会先调用所属类的一个类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector;
该方法的参数就是 objc_msgSend
无法处理的 selector
,返回的布尔值表示这个类能否新增一个实例方法处理它。如果这个 seletor
不是一个实例方法而是一个类方法,那么会有个类似的类方法调用:
+ (BOOL)resolveClassMethod:(SEL)selector;
对象方法的动态方法决议在iOS objc_msgSend 慢速查找流程分析已经介绍过了,类方法的动态方法决议因为是在类的元类当中找方法,与对象方法稍有不同,需要注意一下:
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%@ 来了",NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
三、快速转发 Fast forwarding
(备援接收者)
在找备援接收者之前需要先了解一下其触发条件:
- 方式1:
objcMsgLogEnabled
控制开关 - 方式2:使用
extern
函数声明(extern
可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。)
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
在这一步中,runtime
系统会提供一个方法,让当前接收者返回一个备援接收者来处理未知的 selector
,这个方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;
如果当前接收者能找到或者提供这样一个对象,就将其返回,不能就返回nil。
如果LGTeacher 类
中没有 sayHello 方法
,LGStudent 类
中有,就可以找LGStudent 类
来做备援接收者,进行消息快速转发。
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [LGStudent alloc];
}
四、完整消息转发 Normal forwarding
如果我们没有找到合适的备援接收者,那么就会来到消息慢速转发流程来启动完整的消息转发机制:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
启动完整的消息转发机制时,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。所以,需要配合 forwardInvocation: 方法
搭配使用:
- (void)forwardInvocation:(NSInvocation *)anInvocation
实现这个方法很简单,只要改变目标,让消息在新目标中调用即可。也可以通过改变参数、更换 selector
等,变得应用场景更加多变。
forwardInvocation: 方法
就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,不对外响应和抛出错误。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
// GM sayHello - anInvocation - 漂流瓶 - anInvocation
anInvocation.target = [LGStudent alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
v@:*
: 这里第一字符v
代表函数返回类型void
,第二个字符@
代表self的类型id
,第三个字符:
代表_cmd的类型SEL
。这些符号可在Xcode中的开发者文档中搜索Type Encodings就可看到符号对应的含义,更详细的官方文档传送门 在这里,此处不再列举了。
五、消息转发流程总结
- 若对象无法响应某个选择子,则进入消息转发流程。
- 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”
- 第二阶段涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时运行期系统会请求接收者以其他手段来处理与消息相关方法的调用。为分两小步:
1):请接收者看看有没有其他对象能处理这条消息,若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。
2):若没有“备援接收者”,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation
对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。