OC消息转发机制详解

前言

上一篇 OC消息传递机制大致介绍了当调用一个方法的运行过程:

1. 从缓存查找
2. 从方法列表查找
3. 按继承树递归向上查找方法列表

以上三条如果都没有查找到 则系统提供三次容错方案

1.动态方法解析

@interface TestClass : NSObject

-(void)run;
+(void)go;

@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        TestClass *class =[[TestClass alloc]init];
        [class run];
//        [TestClass go];
    }
}

上述我声明了一个TestClass 并在他的.h中声明了一个 run的实例方法和go的类方法,并且都没有实现。
根据我们之前所了解 从缓存,从本类方法列表,从父类以及基类都无法找到该方法的实现,那么就进入消息转发的第一阶段动态方法解析


    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

根据OC的源码我们可以很清楚的了解 如果没有找到方法的实现,便尝试解析解析一次 如果解析过了triedResolver = YES; 将不会进行解析第二次 那么我们接着看一下 _class_resolveMethod(cls, sel, inst); 到底是怎么做了什么操作


/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

通过代码以及注释我们得到的信息很明显 首先判断是否是元类 如果不是调用+resolveInstanceMethod.如果是元类则调用+resolveClassMethod,这两个方法一个是处理实例方法,一个是处理类方法,那么根据上面的代码,我们重写
+resolveInstanceMethod 进行动态添加方法

@implementation TestClass


+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(run)) {
        class_addMethod(self, sel, (IMP)newRun, "v@:@ ");
    }
    return [super resolveInstanceMethod:sel];
}

void newRun(id self , SEL _cmd) {
    NSLog(@"调用了%@的%@方法",self,NSStringFromSelector(_cmd));
}

@end

调用了的run方法

runtime添加方法请自行百度
经过log我们能够确定 问题已经解决了

那么接下来看类方法是怎么处理的

int main(int argc, char * argv[]) {
    @autoreleasepool {
      [TestClass go];
    }
}
@implementation TestClass

+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel ==@selector(go)){
         class_addMethod(object_getClass(self), sel, (IMP)newGoMethod, "v@:@ ");
    }
    return [super resolveClassMethod:sel];
}

void newGoMethod(id self, SEL _cmd){
     NSLog(@"调用了%@的%@方法",self,NSStringFromSelector(_cmd));
}
@end


调用了TestClass的go方法

这里需要注意的一点 添加类方法和添加实例方法不一样

class_addMethod(self, sel, (IMP)newRun, "v@:@ ");
class_addMethod(object_getClass(self), sel, (IMP)newGoMethod, "v@:@ ");

为什么要使用object_getClass(),因为类的实例方法存在类中,而类的类方法则存在于元类中。对元类来说 类的类方法就是元类的实例方法(有些绕口 请大家自行体会。)

2.备用接收者

如果上述没有做动态方法解析则会进入消息转发第二阶段备用接受者调用方法

- (id)forwardingTargetForSelector:(SEL)aSelector

通过这个方法我们可以移交一个备用者来处理代码如下


@implementation TestClass

-(id)forwardingTargetForSelector:(SEL)aSelector{
    
    NSString *selName =NSStringFromSelector(aSelector);
    if ([selName isEqualToString:@"run"]) {
        return   [[NewTestClass alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

@implementation NewTestClass
-(void)run{
    
       NSLog(@"我是备胎 %@",self.class);
}
@end

[8847:390734] 我是备胎 NewTestClass

3.完整消息转发

如果之前的步骤都没有完成对消息的处理 那么就进入第三部完整消息转发,
完整消息转发会分为两部分

- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation 方法中选择将消息转发给其它对象。

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。具体实现如下

@implementation TestClass

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    //如果返回为nil则进行手动创建签名
    if ([super methodSignatureForSelector:aSelector]==nil) {
        NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sign;
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    //创建备用对象
    NewTestClass * newClass = [NewTestClass new];
    SEL sel = anInvocation.selector;
    //判断备用对象是否可以响应传递进来等待响应的SEL
    if ([newClass respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:newClass];
    }else{
        // 如果备用对象不能响应 则抛出异常
        [self doesNotRecognizeSelector:sel];
    }
}
@end

@implementation NewTestClass
-(void)run{
    
       NSLog(@"我是备胎 %@",self.class);
}
@end

[8847:390734] 我是备胎 NewTestClass

以上就是完整的消息转发过程。

最后附上一张消息转发的步骤图


image.png

你可能感兴趣的:(OC消息转发机制详解)