OC-Runtime:iOS 的消息转发流程篇 讲述了消息在发送阶段的转发流程,这里会结合实例,更直观的看一下消息转发的流程。
在 ViewController.m
文件里调用一个不存在的消息
- (void)viewDidLoad { |
动态添加方法的实现
可以在 ViewController.m 里重写 resolveInstanceMethod:
,给对应 selector 动态添加实现,不要忘了导入运行时库。
|
ExceptionHandler.m 类
// ExceptionHandler.m |
运行查看输出
--------1.-----------[ViewController] - [cus_test:desc:] |
这这里有几点需要注意:
根据 selector 区别方法,只对用户自定义方法动态添加,如果不加以区分的话,会影响到系统方法,如
setStoryboard:
、setValue:forKey:
方法的实现可以在本类提供,也可以在其他类提供。这里的
ExceptionHandler
里的catchException
方法会打印出调用者和方法名。官方示例文档上面解释说当给接受者成功添加实现的时候返回 YES,否则返回 NO。
Returns
YES if the method was found and added to the receiver, otherwise NO.
网上几乎所有的资料都解释说返回 YES 的时候,消息转发不会在继续后面的流程。但是在实验的阶段,动态添加方法成功的同时返回 NO,消息转发同样没有继续后面的流程了。这里还蛮疑惑的,我试着看会不会走到父类的 forwardingTargetForSelector:
,同样的也是没有的。在 return NO
的地方单步调试
从调用栈来看,在判断是否 resolveInstanceMethod
之后又进行了一次查找方法的 IMP 的操作,第二次会找到对应 IMP ,虽然这里有二次寻找,但是这个 IMP 是否有被执行呢?结合 Runtime 的源码
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) |
但是这似乎并不能解释 IMP 为什么会被执行。在一步一步的调试中发现,最后都是到寄存器执行 IMP 的,在 x86_64s 的架构上都是到 r11 寄存器上的
_objc_msgSend_uncached
的汇编代码
// r10 is already the class to search |
此时的 IMP 就是上面动态添加的方法实现。
调用栈是这样的
结合看 lookUpImpOrForward
和 _class_lookupMethodAndLoadCache3
的源码
objc-runtime.new.mm
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) |
objc-runtime.new.mm
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
注意看上面标出的 ①,这里会再次尝试去寻找 IMP,当然这里是找到 IMP 的,程序继续回到 _objc_msgSend_uncached
的寄存器上执行。
从这些可以看出,+ resolveInstanceMethod:
的返回值并没有影响到消息转发的流程。
转发到新的对象
在 + resolveInstanceMethod:
方法里不动态添加方法的实现,消息转发会 走_objc_msgForward
转发到自定义对象。
- forwardingTargetForSelector:
是第一个被调用的方法。引用官方的摘要:
forwardingTargetForSelector
Summary
Returns the object to which unrecognized messages should first be directed.
意思就是把这个不识别的消息转发到一个新的对象去执行。这个需要我们返回一个已经实现了对应 selector 的实例对象。
- (id)forwardingTargetForSelector:(SEL)aSelector { |
在 ExceptionHandler.m 需要实现方法
- (NSInteger)cus_test:(NSString *)msg desc:(NSString *)desc { |
结合 Runtime,当没有找到 IMP 的时候,在 objc-runtime.new.mm 的 lookUpImpOrForward
函数会返回 _objc_msgForward_impcache
,首先通过 objc_msgSend
执行
// r10 is already the class to search |
此时的 IMP 就是 _objc_msgForward_impcache
,然后跳转到 _objc_msgForward_impcache
去执行,就是👇
id _objc_msgForward(id self, SEL _cmd,...);
_objc_msgForward
and_objc_msgForward_stret
are the externally-callable functions returned by things like method_getImplementation().
_objc_msgForward_impcache
is the function pointer actually stored in
method caches.
STATIC_ENTRY __objc_msgForward_impcache |
首先会判断是否实现了 forwardingTargetForSelector:
方法,然后调用 forwardingTargetForSelector:
。再然后用其返回的对象调用方法,就是正常的消息分发流程了。
运行查看输出
--------1.-----------[ViewController] - [cus_test:desc:] |
forwardInvocation
当 forwardingTargetForSelector:
返回为 nil
的时候,消息转发会继续到 methodSignatureForSelector:
方法,获取方法签名,成功获取到方法签名会继续下面的流程。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { |
运行查看输出
--------1.-----------[ViewController] - [cus_test:desc:] |
这里出现了一次 resolveInstanceMethod:
的打印, 对应 _forwardStackInvocation:
方法,是内部调用的私有方法,这里可以忽略掉。
当 methodSignatureForSelector:
返回为 nil
的时候,会到 doesNotRecognizeSelector:
,程序 crash。
- (void)doesNotRecognizeSelector:(SEL)aSelector { |
运行查看输出
--------1.-----------[ViewController] - [cus_test:desc:] |
总结与思考
从消息从调用到执行的整个流程来看,大致可以分为两个阶段:第一阶段就是执行 objc_msgSend
阶段,这个阶段在主要通过 lookUpImpOrNil
来找到方法对应的 IMP 去执行,如果没找到,提供一次动态添加方法实现的机会;如果最终没有 IMP,会走 _objc_msgForward
进行消息转发给新的 target 去实现。
专门的异常处理
resolveInstanceMethod
动态给方法添加实现,在这里处理的好处是,你可以统一将没有实现的方法都抛给一个专门处理这类异常的类去处理,例如上面的 ExceptionHandler
。
方法签名与参数修改
在消息转发阶段,被转发的对象都需要实现同名的方法。一般都是在 forwardInvocation:
处理消息转发,在这里处理的好处是可以通过 NSInvocation
类拿到所有的参数,你也可以在这里修改参数。
在实践过程中,
methodSignatureForSelector:
生成方法签名的时候,也可以直接通过字符串而不通过某个具体的类生成,这个时候需要你保证 Type Encodings 是对应上的,虽然没有对应上也能成功,但是会对forwardInvocation:
有影响。- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"--------3.-----------[%@] - [%@]", NSStringFromClass([self class]), NSStringFromSelector(aSelector));
// ExceptionHandler *handler = [[ExceptionHandler alloc] init];
// if ([handler respondsToSelector:aSelector]) {
// return [handler methodSignatureForSelector:aSelector];
// }
NSString *selName = NSStringFromSelector(aSelector);
if ([selName hasPrefix:forwardPrefix]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@@"];
}
return [super methodSignatureForSelector:aSelector];
}在
forwardInvocation:
中可以对入参进行修改。关于为什么入参的下标从 2 开始,OC 方法里默认有
self
和_cmd
两个参数,方法的入参从第三个开始,即下标为 2 开始。NSInvocation
的参数传递与方法签名对应,所以虽然方法签名可以通过字符串生成,但是最好还是要和方法对应上。- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"--------4.-----------[%@] - [%@]", NSStringFromClass([self class]), NSStringFromSelector(anInvocation.selector));
NSString *selName = NSStringFromSelector(anInvocation.selector);
if ([selName hasPrefix:forwardPrefix]) {
id target = [[ExceptionHandler alloc] init];
NSInteger numberOfArguments = anInvocation.methodSignature.numberOfArguments;
if (numberOfArguments > 2) {
for (int i = 2; i < numberOfArguments; i ++) {
const char *argumentType = [anInvocation.methodSignature getArgumentTypeAtIndex:i];
if (strcmp(argumentType, "@") == 0) {
NSString *argument = [NSString stringWithFormat:@"test%d", i];
[anInvocation setArgument:&argument atIndex:i];
} else if (strcmp(argumentType, "i") == 0) {
[anInvocation setArgument:&i atIndex:i];
}
}
}
[anInvocation invokeWithTarget:target];
} else {
[super forwardInvocation:anInvocation];
}
}运行查看输出
test message: test2; test3
应用
现在比较流行的切面编程(AOP)–Aspects 就是依赖 Method Swizzling 和 _objc_msgForward
实现的。
Demo 在这里。