ios 底层原理之消息转发

234 阅读2分钟

前言

在上一篇底层原理之动态方法决议我们说消息发送先经过objc_msgSend快速查询IMP,查询不到IMP就进入lookUpImpOrForward慢速查询,如果慢速查询再查询不到,苹果爸爸贴心的给我们一次机会,叫动态方法决议,只要在动态方法决议中实现了IMP,就可以防止方法找不到的崩溃。那么动态方法决议中没有实现IMP的话,苹果爸爸还会不会再给个机会呢?oc源码

msgSend日志

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}

分析:我们在探索消息的慢速查询时,如果查询到了IMP会调用log_and_fill_cache插入缓存。如果objcMsgLogEnabled==true且implementer存在,就会调用logMessageSend方法插入日志,源码搜索logMessageSend如下:

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char buf[ 1024 ];
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();
    return false;
}

分析:当进行消息发送的时候,会创建一个/tmp/msgSends-%d的日志记录,那么如果想要查看日志objcMsgLogEnabled必须为true。全局搜索一下objcMsgLogEnabled发现instrumentObjcMessageSends()函数控制了objcMsgLogEnabled的值

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;
    if (objcMsgLogEnabled == enable)
        return;
    if (enable)
        _objc_flush_caches(Nil);
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);
    objcMsgLogEnabled = enable;
}

分析:所以想要消息发送输出日志,可以向该函数传true,想要停止输出日志可以传false

写个demo试一下:新建LGPerson类,初始化并调用不存在的方法sayHello()

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

分析:在private/tmp文件夹下果然发现一个msgSends-8209文件。打开如下

+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:
.....

分析:调用resolveInstanceMethod动态方法解析之后,系统先会调用forwardingTargetForSelector,然后再调用methodSignatureForSelector。这就是我们今天探索的主题消息转发。

forwardingTargetForSelector快速转发

源码全局搜索该函数发现只有方法声明没有实现,应该是在cf框架里不开源

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

xode:command+shift+0 搜索一下该函数的定义

# forwarding​TargetForSelector:
Returns the object to which unrecognized messages should first be directed.

分析:forwardingTargetForSelector含义是返回未识别消息重定向的对象,简单理解指定一个对象,让这个对象去接收这个消息

接着上面的LGPerson实例,我们再写一个GyTest类实现sayHello()方法,通过forwardingTargetForSelector转发到GyTest看是否可以。

@implementation LGPerson
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if([NSStringFromSelector(aSelector) isEqual:@"sayHello"]){
        return [[GyTest alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

输出:

**---[GyTest sayHello]---**

分析:通过forwardingTargetForSelector把消息转发给类GYTest,让其实现SEL的IMP果然是可以的。注意sayHello()是对象方法,如果调用的是类方法,就要通过+(id)forwardingTargetForSelector把消息转发出去。GYTest收到消息会重新进入消息查找流程,快速、慢速、动态方法决议、消息转发。

methodSignatureForSelector慢速转发

源码搜索该函数同样有两个没有源码的类方法和对象方法。xode:command+shift+0 搜索一下该函数的定义

# method​SignatureForSelector:
Returns an `NSMethodSignature` object that contains a description of the method identified by a given selector.
....
....
[`- forwardInvocation:`]()
Overridden by subclasses to forward messages to other objects.

分析:methodSignatureForSelector的含义是返回一个NSMethodSignature对象。methodSignatureForSelector一般搭配和forwardInvocation使用,如果methodSignatureForSelector方法返回的是一个nil就不会调用forwardInvocation

接着上面的LGPerson实例,注释掉快速转发

@implementation LGPerson
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if([NSStringFromSelector(aSelector) isEqual:@"sayHello"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"进入慢速转发:%@---%@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
    if( [NSStringFromSelector(anInvocation.selector) isEqual:@"sayHello"]){
        GyTest* test=[[GyTest alloc]init];
        anInvocation.target=test;
        //如果想实现GyTest其他方法的话
        // anInvocation.selector=@selector(anyotherway);
        [anInvocation invoke];
    }
}

输出:

进入慢速转发:<LGPerson: 0x10075df90>---sayHello
 ---[GyTest sayHello]---

分析:

  • methodSignatureForSelector返回NSMethodSignature实例,那么就会进入forwardInvocation方法,如果返回为nil,那么就不会进入forwardInvocation方法。
  • 只要进入了forwardInvocation方法就可以防止方法找不到的奔溃不管有没有实现消息转发
  • forwardInvocation中的NSInvocation对象保存着消息接收者和SEL,可以重定向消息接收者和SEL,调用invoke实现消息的转发。

补充

消息发送的流程已经分析结束了,补充一张流程图帮助记忆和理解

未命名文件-2.png

总结:

  • 快速转发forwardingTargetForSelector实现对象的重定向,进入指定对象的方法查找流程,如果返回是nil,进入慢速转发流程

  • 慢速转发methodSignatureForSelector返回值是nil,慢速查找流程结束。如果有返回值就进入forwardInvocation,只要进入此方法就可以避免方法找不到的奔溃,可以重定向消息接收者和SEL,调用invoke实现消息的转发。