iOS看源码:消息转发

2020-07-08  本文已影响0人  FireStroy

消息的发送

前篇
iOS看源码:消息发送01
iOS看源码:方法缓存
iOS看源码:方法慢速查找
消息发送的本质是objc_msgsend(),会先从消息接受者的缓存中查找,缓存中找不到则按照isa的指向依次按照由本类向父类直到根类NSObject的方法列表中查找。

消息动态转发

lookUpImpOrForward()各种流程都没找到方法实现 那么就会返回一个系统默认的(IMP)_objc_msgForward_impcache方法实现。
这就是让你看到*** unrecognized selector sent to instance ***这条崩溃信息的函数。

但是 ,在程序崩溃之前,你还是有为这条没有找到IMP的方法提供一个IMP的机会。

当上面的消息查找流程没有找到IMP时,会有一次时机执行方法的转发机制。

    // No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

动态转发过程

通过对标志位behavior的判断和操作,对没有进行过动态转发的消息进行一次转发机制resolveInstanceMethod ()

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    runtimeLock.unlock();
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

当走到这一步的时候,runtime会先查询对象是否实现了resolveInstanceMethod()这个方法,如果实现了这个方法就直接调用这个方法。
然后会再执行一次方法的查找流程lookUpImpOrNil()
这里为什么要再执行一次查找流程呢?
因为resolveInstanceMethod()这个方法就是提供一个让你提供一个被查找方法的IMP的机会。

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    IMP imp = lookUpImpOrNil(inst, sel, cls);
    if (resolved  &&  PrintResolving) {
      //...
    }
}

动态决议第一阶段

先调用resolveInstanceMethod 为对象提供一次解决方案,如果对象实现了个方法并且提供了所查找方法的IMP则再次进行lookUpImpOrNil()方法查找流程继续走消息发送正常流程。

用代码演示一下:

@interface MyPerson : NSObject
- (void)say;
@end

@implementation MyPerson
@end

int main(int argc, const char * argv[]) {
    MyPerson *person = [[MyPerson alloc]init];
    [person say];
    return 0;
}

person发送了一条没有实现过的say()方法程序直接崩溃

接下来让Person类实现了+ resolveInstanceMethod();方法并且添加了一个- cry()的方法实现,并且吧这个实现作为@selector(say)的方法实现。

- (void)cry{NSLog(@"say-->cry");}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say)) {
        IMP sayCry = class_getMethodImplementation(self, @selector(cry));
        Method method = class_getInstanceMethod(self, @selector(cry));
        const char* type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayCry, type);
    }
    return [super resolveClassMethod:sel];
}

结果就是程序没有崩溃,原来的-say方法最终调用的是-cry方法的实现。

消息转发成功

这一阶段的转发,需要注意类方法和实例方法 不过最后的本质是一样的,只是区别于类和元类,最后的根元类也最终指向了NSObject类。
注意,如果你把这个转发过程放在了NSObject的分类中去实现而不不加处理,那么你可能就覆盖了很多系统级别的消息转发。

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

第二阶段

如果第一个阶段,我们并没有去实现那怎么办?
再开始这一阶段讲解之前,先讲一个小技巧。
前面在讲解方法缓存的时候会遇到这一步代码

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
    cache_fill(cls, sel, imp, receiver);
}

这里SUPPORT_MESSAGE_LOGGING提示我们logMessageSend()是一个方法日志,它可以吧方法的调用记录下来。

开启的方法演示一下:

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    MyPerson *person = [[MyPerson alloc]init];
    instrumentObjcMessageSends(true);
    [person say];
    instrumentObjcMessageSends(false);
    return 0;
}

这样我们就可以查看方法崩溃前,都调用过什么。
回到第一步的崩溃案例,在崩溃方法前开启日志打印,这样会在Mac的tmp目录下记录调用过程。

log日志

我们看到 崩溃前,调用了几个我们并不常见到的方法

  1. + resolveInstanceMethod
  2. - forwardingTargetForSelector
  3. - methodSignatureForSelector
  4. - doesNotRecognizeSelector
    第一个我们知道了,就是动态转发的第一个步骤。
    那么接下来的2、3、4是做什么的呢?
    NSObject的头文件供了这些方法。
- (void)doesNotRecognizeSelector:(SEL)aSelector;
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

具体怎么用去看看官方文档提供的信息。


动态决议二阶段-快速转发

- forwardingTargetForSelector:这个方法叫我们找一个能够解决这个SEL实现的对象。自己不能解决,或许可以交给别人去解决。
自己的类没有实现-say这个放么,把问题甩给MyStudent的实例对象去解决。相应的MyStudent这个类如果实现了这个甩过来的方法,就会执行调用。
代码演示:

@interface MyStudent : NSObject
- (void)say;
@end

@implementation MyStudent
- (void)say{NSLog(@"student - say");}
@end

@implementation MyPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(say)) {
        return [[MyStudent alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
person把消息给了student并调用

第三阶段

第二阶段讲了,自己没有实现的消息可以转发给其他实现了这个消息的对象。那么万一没有人实现了这个消息怎么办?
如果第二阶段失败,那么就进入第三阶段 消息的慢速转发

动态决议第三阶段-消息的慢速转发

- methodSignatureForSelector:方法签名的处理
消息到了这一步会获取到一个NSInvocation对象,里面包装了方法名和一些参数,这时候你可以选择让合适的对象来处理它invoke,当然也可以不调用。最少可以把程序崩溃在这里处理了。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(say)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL aSelector = [anInvocation selector];
    id obj = [MyStudent alloc];
       if ([[MyStudent alloc] respondsToSelector:aSelector])
           [anInvocation invokeWithTarget:[MyStudent alloc]];
       else
           [super forwardInvocation:anInvocation];
}

最后的崩溃

如果你没有实现第三步那么程序就真的要走崩溃流程了
- doesNotRecognizeSelector:然后闪退。

上一篇下一篇

猜你喜欢

热点阅读