前言
在前两篇文章中,我们了解了快速查找和慢速查找流程。那么当快速查找流程和慢速查找流程都结束了,仍然没有找到响应的方法时,会怎么办呢。苹果给出了两个建议,就是我们本次要研究的动态方法决议和消息转发。
动态方法决议:慢速查找流程未找到时,会执行一次动态方法决议,动态方法决议之后会重新在进行一次慢速查找流程。消息转发:如果动态方法决议仍然没找到实现,则进行消息转发,消息转发又分为快速转发流程和慢速转发流程。
准备用的代码:
// 自定义Person类
@interface Person : NSObject
- (void) running;
- (void) swimming;
@end
@implementation Person
- (void) running
{
NSLog(@"running");
}
@end
// 自定义Student类继承自Person
@interface Student : Person
- (void) swimming;
@end
@implementation Student
- (void) swimming
{
NSLog(@"swimming");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
// 方法调用
[person running];
[person swimming];
}
return 0;
}
方法未实现报错分析
按如下步骤执行代码:
- 分别定义Person类和继承自Person的Student类;
- Person类定义两个方法,分别为running和swimming方法;
- Person类中我们只实现running方法,不实现swimming方法;
- Student类中我们定义并实现swimming方法;
- 然后我们分别用person对象调用running和swimming方法;
- running方法正常执行,由于swimming方法没有被实现所以报错。
报错探究
我们先看一下,消息慢速查找完成后会走到__objc_msgForward_impcache方法。
__objc_msgForward_impcache源码如下
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
__objc_msgForward_impcache方法中我们看到,方法内只调用了__objc_msgForward方法。
__objc_msgForward源码如下
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
__objc_msgForward方法中调用了__objc_forward_handler方法。
通过搜搜该方法,并未找到实现,所以我们去掉下划线,查找他的C++方法,objc_forward_handler,我们在objc-runtime中查找到了该方法,源码如下:
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
其本质上调用的是objc_defaultForwardHandler方法,我们在来看一下他的实现:
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
我们可以发现,这里返回的错误信息就是我们开发过程中常见的方法未实现时被调用报的错误信息。
接下来我们研究一下,如果在崩溃前进行处理,就是我们今天的第一个主角动态方法决议。
动态方法决议
当慢速查找结束后,就回来来到动态方法决议的流程,这也是我们可以控制的第一次容错或者改善错误的地方。
动态方法决议分析
源码
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 判断是否是元类,也就是查找对象方法还是实例方法
// 此处如果不是元类,则调用对象方法
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
// 如果是类则调用实例方法,也就是元类的对象方法
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
// 这里就是isa走位图类方法调用到跟元类时,会向类方法中进行查找
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
流程解析
- 首先判断是否是元类
-
- 如果是
类,执行实例方法的动态方法决议resolveInstanceMethod; - 如果是
元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没找到或者元类为空,则在元类对应的类执行实例方法动态方法决议。
- 如果是
- 动态方法决议完成,会调用
lookUpImpOrForwardTryCache方法先查询一次缓存,如果没有缓存就会再次来到lookUpImpOrForward,再次进行一次慢速查找。
动态方法解析流程图
代码实现
实例方法
// 添加一个swimming2方法,用于动态方法决议的时候为sel绑定imp
- (void) swimming2
{
NSLog(@"swimming2");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"来到了动态方法决议");
// 判断是否是swimming方法
if ([NSStringFromSelector(sel) isEqualToString:@"swimming"]) {
NSLog(@"%s--动态方法决议", sel);
// 动态添加方法实现
// 获取swimming2方法的imp
IMP imp = class_getMethodImplementation(self, @selector(swimming2));
// 动态添加方法
// method_getTypeEncoding获取方法签名 == “v@:”
const char *type = method_getTypeEncoding(class_getInstanceMethod(self, @selector(swimming2)));
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
打印结果
- 当慢速查找结束后,会来到动态方法决议;
- 通过判断方法名称来做相应的处理;
- 获取我们要使用的方法的imp;
- 并获取我们要使用的方法的方法签名"v@:";
- 为sel动态添加imp实现并返回;
- 当添加成功后重新进行慢速查找流程;
- 由于已经添加了新的方法实现,所以在第二次的慢速查找时就会成功查找到方法imp。
类方法
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"来到了类方法动态方法决议");
if ([NSStringFromSelector(sel) isEqualToString:@"jump"]) {
NSLog(@"%s--动态方法决议", sel);
// 动态添加方法实现
// 获取swimming2方法的imp
IMP imp = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(jump2));
// 动态添加方法
const char *type = "v@:";
// 向元类的方法列表中添加方法
return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
打印结果
创建Person扩展,并实现resolveInstanceMethod也可以处理,完美验证isa走位图,根源类中没找到实现后,会再次到根元类的类的方法列表中进行查找。
#import "NSObject+Person.h"
#import "message.h"
@implementation NSObject (Person)
- (void)jump3
{
NSLog(@"jump3");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"来到了动态方法决议");
// 判断是否是swimming方法
if ([NSStringFromSelector(sel) isEqualToString:@"jump"]) {
NSLog(@"%s--动态方法决议", sel);
// 动态添加方法实现
// 获取swimming2方法的imp
IMP imp = class_getMethodImplementation(self, @selector(jump3));
// 动态添加方法
// method_getTypeEncoding获取方法签名 == “v@:”
const char *type = method_getTypeEncoding(class_getInstanceMethod(self, @selector(jump3)));
return class_addMethod(self, sel, imp, type);
}
return [self resolveInstanceMethod:sel];
}
@end
打印结果
消息转发
此处不做流程探究,直接讲解消息转发的使用流程。
消息快速转发:forwardingTargetSelector方法消息慢速转发:methodSignatureForSelector+forwardInvocation
消息转发流程图
- 动态方法决议失败后,会来到消息转发流程;
- 首先进入消息快速转发
-
- 快速转发中可以为消息设置新的消息接受者;
- 设置完消息接受者后系统会到新的消息接受者中查找方法实现;
- 如果快速转发返回nil,会进入到慢速转发流程
-
- 先
methodSignatureForSelector返回新的方法签名; - 再在
forwardInvocation中设置target或者selector等信息;
- 先
-
- 配置好后调用
invoke方法;
- 配置好后调用
- 如果快速转发和慢速转发都没有处理,则会报错方法未实现。
快速转发代码实现:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"来到了--forwardInvocation");
if ([NSStringFromSelector(aSelector) isEqualToString:@"swimming"]) {
// 将消息接受者转给Student,再次进行方法查找时会到Student中进行查找
return [Student alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
- forwardingTargetForSelector中判断需要处理的方法;
- 创建新的消息接受者并返回;
- 当设置号新的消息接受者后会重新进行慢速查找。
※
-
此处转发时,如果新的消息接受者中也没实现这个方法就会出现死循环。-
虽然是消息接受者进行了改变,但是消息处理还是当前类; -
所以当新的消息接受者未实现时,慢速查找还会失败,还会走到消息转发流程; -
消息转发还会返回这个对象,所以会导致死循环的产生。
-
慢速转发代码实现
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"来到了methodSignatureForSelector方法签名");
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"来到了forwardInvocation");
// 设置Target消息接受者
// anInvocation.target = [Student alloc];
// 设置消息实现方法
anInvocation.selector = @selector(swimming2);
[anInvocation invoke];
}
- methodSignatureForSelector中返回方法签名;
- 返回方法签名后回来到forwardInvocation中;
- 在forwardInvocation中我们对消息接受者或者消息进行设置。
动态方法决议&消息转发流程图总
、