iOS-动态方法决议与消息转发机制

在消息发送机制中我们介绍过,OC是动态语言,对象调用方法其实就是对象接收消息,而消息的发送采用“动态绑定”机制,具体会调用哪个方法直到运行时才确定,然后去执行绑定的代码。

绑定的过程:

以下面代码为例

Person *p = Person.new;
[p eat];

方法调用时,编译器会底层将之转换成C语言的函数objc_msgSend(p, @selector(eat));,来对p发送消息。消息接收者p对象通过方法的名称SEL(即eat),首先到该类的方法cache中查找对应的方法实现IMP,如果找到就执行该方法实现;如果没找到就到该类的方法列表(methodLists)中去找,如果找到就执行该方法实现并且缓存到cache中;如果没找到就通过superclass指针到其父类的方法cache和methodLists中去找,如果找到就执行该方法实现并且缓存到cache中;如果在其所有父类的方法cache和methodLists中都没找到对应的IMP,那么程序就会crash!!!
显然crash不是我们想看到了,如何来阻止闪退呢?也就是今天我们要介绍的动态方法决议!

动态方法决议:

还是上面的代码,如果Person类中没有eat方法的实现,会怎么样的?

-[Person eat]: unrecognized selector sent to instance 0x6000012b0460
 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person eat]: unrecognized selector sent to instance 0x600002fa0400'

造成 crash 的原因:对象无法处理 eat 对应的 selector,也就是没有找到相应的方法实现。

Object-C为我们提供一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个 selector 提供实现。我们只要实现 +resolveInstanceMethod: 和/或 +resolveClassMethod: 方法,并在其中为指定的 selector 提供实现即可(通过调用运行时函数 class_addMethod 来添加)。这两个方法都是 NSObject 中的类方法:

// 类方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); 
// 对象方法进行决议
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

sel表示要决议的方法名,返回值文档中说是表示动态决议成功与否。如果在该函数内为指定的 selector 提供实现,无论返回 YES 还是 NO,编译运行都是正确的;但如果在该函数内并不真正为 selector 提供实现,无论返回 YES 还是 NO,运行都会 crash,道理很简单,selector 并没有对应的实现。
注意:只有当编译器没有找到 selector 对应的方法实现的时候才会进入动态方法决议,如果决议之前找到对应实现就不会进入动态方法决议。

// 当调用未实现的对象方法,回调该动态方法决议
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolve instance method --- %@",NSStringFromSelector(sel));
    if (sel == @selector(eat))
    {
        // 方式一:调用下面 C 函数
        //class_addMethod(self.class, sel, (IMP)dynamicResolveMethod, "v@:@");
        //方式二 :调用下面 OC 函数  class_getMethodImplementation 改变现有的实现
        IMP imp = class_getMethodImplementation(self.class, @selector(dynamicResolveMethod));
        class_addMethod(self.class, sel, imp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
// 实现 C 函数
void dynamicResolveMethod(id self, SEL _cmd) {
    NSLog(@"C 函数添加成功");
}

// 实现 OC 方法
- (void)dynamicResolveMethod
{
    NSLog(@"OC 对象方法添加成功");
}

方法一:Objective C 中的方法其实就是至少带有两个参数(self 和 _cmd)的普通 C 函数,因此在上面的代码中提供这样一个 C 函数 dynamicResolveMethod,让它来充当对象方法 eat 这个 selector 的动态实现。
方法二:selector动态绑定已经存在的对象方法的IMP,可以使用class_getMethodImplementation()函数:

OBJC_EXPORT IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

cls是Person类,SEL是Person类中已经存在的方法实现对应的方法名,返回值是函数指针IMP,然后把该IMP动态的绑定到eat的selector。这个办法有个好处就是当我们测试动态方法决议也没有找到eat 的动态实现的时候,不会编译报错。如果使用(IMP)dynamicResolveMethod的形式,而且没有dynamicResolveMethod函数,即没有该方法实现,会编译报错。
如果我们把上面的方法实现代码注释掉,即在动态方法决议的时候仍然不给eat提供相应的IMP,效果会怎么样?

 -[Person eat]: unrecognized selector sent to instance 0x600000ae48a0

运行起来后,程序crash,原因和之前一样,没有找到eat的对应的方法实现,怎么来再挽救这次闪退呢?runtime又为我提供一种挽救的方式:消息转发。

消息转发:

消息转发就是在编译器没有找到eat对应的方法实现的时候,会把我们的消息转发出去,具体转发给谁,需要我们自己设定,来看下消息转发的方法:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    Dog *dog = [[Dog alloc] init];
    if ([dog respondsToSelector:aSelector])
    {
        return dog;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@implementation Dog

- (void)eat{ 
    NSLogo(@"eat");
}

@end

代码解释:消息转发方法- (id)forwardingTargetForSelector:(SEL)aSelector,该方法会在动态放决议也解决不了的时候回调(系统自动回调),在该方法中来实现消息转发的功能,参数aSelector为eat的selector,返回值为id类型,表示转发的目标对象(备用的消息处理者:dog对象)。上述代码中,我们创建一个Dog类,在Dog类的实现代码中,添加一个eat方法实现,目的是为了动态绑定eat的selector相应的方法实现。
再运行程序,程序没有异常。如果我们Dog类中的没有eat的selector对应的IMP又会怎么样呢?结果会crash,原因依然是:

 -[Person eat]: unrecognized selector sent to instance 0x600000ae48a0

这时候runtime还会为我们提供最后的防线来防止程序异常,启用完整的消息转发机制

// 如果签名 不为nil ,那么runtime会创建一个 NSInvocation 对象,并发送forwardInvocation:消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    //查找父类签名
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];

    if (methodSignature == nil)
    {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return methodSignature;

}

// invocation 调用
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL sel = anInvocation.selector;

    Person *person = Person.new;

    if ([person respondsToSelector:sel])
    {
        [anInvocation invokeWithTarget:person];
    } else {
        NSLog(@"真的找不到 %@ 的方法实现!!!",NSStringFromSelector(sel));
    }
}

// 当方法签名为nil,调用此方法。程序crash
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
    NSLog(@"程序crash了");
}

代码解释:系统向对象发送methodSignatureForSelector:消息,传入eat方法的selector,返回一个方法签名,如果方法签名为nil,则会调用doesNotRecognizeSelector:方法,程序crash;如果方法签名不为空,系统会创建一个 NSInvocation 对象,并发送forwardInvocation:消息。对象anInvocation中保存着方法的信息(方法签名methodSignature、方法名SEL、消息接收者target等),判断Person类中是否有该selector相应的方法实现,如果有就调用 NSInvocation 的invokeWithTarget:方法,如果没有就说明真的找不到eat方法的方法实现了,runtime只能帮到这里,虽然没有得到响应,但是程序进程是保住了,没有出现crash。总结如下图:
iOS-动态方法决议与消息转发机制_第1张图片
总结

通过上面介我们知道动态方法决议是先于消息转发的,如果向一个对象发送它无法处理的消息(selector),那么Runtime会按照如下次序进行处理:首先看是否为该 selector 提供了动态方法决议机制,如果提供了则进行动态方法决议,且真正为该 selector 提供了实现,那么就调用该实现,完成消息发送流程;如果没有提供动态方法决议就看是否为该 selector 提供了消息转发机制,如果提供了则进行消息转发,此时,无论消息转发是怎样实现的,程序均不会 crash。(因为消息调用的控制权完全交给消息转发机制处理,即使消息转发并没有做任何事情,运行也不会有错误,编译器更不会有错误提示。);如果没提供消息转发机制,则转到消息重定向,处理消息;如果方法签名为空则程序 crash;

上面介绍的是对象方法的动态方法决议以及消息转发机制,在Objective-C中,类本身就是一个不透明类型的对象,类不能使用前面为实例显示的声明语法定义属性,但它们可以接收消息。如果是类方法如何?基本上一样,有些许差别,类方法的动态方法决议会调用:

+ (BOOL)resolveClassMethod:(SEL)sel{
}

class_addMethod()中要传入Class类型参数:

        // 注意:类方法都在元类中,实例方法在类中
        Class class = objc_getMetaClass(NSStringFromClass(self.class).UTF8String);
        IMP imp = class_getMethodImplementation(class, @selector(classMethodOtherName:));
        class_addMethod(class, sel, imp, "v@:");

消息转发和消息重定位也是类方法:

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{ 
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
}
+ (void)doesNotRecognizeSelector:(SEL)aSelector
{
}

你可能感兴趣的:(技术类博客,消息转发,动态方法决议)