十一、消息转发机制

经历的九、objc_msgSend流程分析快速查找和十、objc_msgSend流程分析慢速查找都未找到方法的IMP, 则会进行消息转发, 苹果为我们提供了几个方法供我们在崩溃之前再次对消息进行处理

消息转发机制.png

1.方法动态决议(会执行两次)

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
....

1.1 判断当前类是否是元类

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 (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

  • 当前类不是元类则 调用resolveInstanceMethod(inst, sel, cls);

  • 当前类是元类则 调用

  resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
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())) {
        // Resolver not implemented.
        return;
    }

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

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

2. 消息转发-消息接受者重定向 forwardingTargetForSelector

在看这个方法之前, 我们会疑问为什么是这个方法而不是别的?
我们借助一个打印函数来查看方法的调用


图片.png

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)done:中我们可以看到这个方法log_and_fill_cache,

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);
}

结合上面克制是否打印的控制条件在于objcMsgLogEnabled, 而objcMsgLogEnabled又由instrumentObjcMessageSends()传入的参数控制, 所以我们可以直接在外部使用这个函数来控制是否打印

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;
}

打印日志在方法中可以看到

图片.png

我们在Finder 中直接搜索 (cmd + shift +g ) /tmp 可以看到目录中多了一个文件
图片.png

感觉明显的可以,删掉这个日志重新跑一边程序,打开看下日志内容
图片.png

我们可以借助官方文档来查看下这个方法 在xcode中 cmd + shift +0(零)

图片.png

大致的意思就是

如果一个对象实现(或继承)这个方法,并返回一个非nil(和非self)结果,那么返回的对象将用作新的接收者对象,消息分派将继续到这个新对象。(显然,如果从这个方法返回self,代码将陷入无限循环。)
如果你在一个非根类中实现这个方法,如果你的类对于给定的选择器没有返回任何东西,那么你应该返回调用super的实现的结果。
此方法让对象有机会重定向发送给它的未知消息,然后再由开销大得多的forwardInvocation: machinery接管。当您只是想将消息重定向到另一个对象时,这是非常有用的,并且可能比常规转发快一个数量级。如果转发的目标是捕获NSInvocation,或者在转发过程中操纵参数或返回值,那么它就没有用了。


图片.png
图片.png

LGPerson中没有实现sayHello, LGStudent中实现了, 我们在LGPerson中重写-forwardingTargetForSelector方法, 并返回一个LGStudent对象, 程序没有崩溃, 日志中可以看出是调用的[LGStudent sayHello]
我们可以利用这一点在APP中做crash的防护, 下面的代码是我在APP中添加防护分类,需要的小伙伴可以借鉴改进

// 自定义实现 `-kb_forwardingTargetForSelector:` 方法, 和`+ (id)kb_forwardingTargetForSelector:(SEL)aSelector`
- (id)kb_forwardingTargetForSelector:(SEL)aSelector {
    
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    
    // 获取 NSObject 的消息转发方法
    Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
    // 获取 当前类 的消息转发方法
    Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
    
    // 判断当前类本身是否实现第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
    
    // 如果没有实现第二步:消息接受者重定向
    if (!realize) {
        // 判断有没有实现第三步:消息重定向
        SEL methodSignature_sel = @selector(methodSignatureForSelector:);
        Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
        
        Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
        realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
        
        // 如果没有实现第三步:消息重定向
        if (!realize) {
            // 创建一个新类
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);
    
            NSLog(@"*** Crash Message: -[%@ %@]: unrecognized selector sent to instance %p ***",errClassName, errSel, self);
            
            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            
            // 如果类不存在 动态创建一个类
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 注册类
                objc_registerClassPair(cls);
            }
            // 如果类没有对应的方法,则动态添加一个
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
            }
            // 把消息转发到当前动态生成类的实例对象上
            return [[cls alloc] init];
        }
    }
    return [self kb_forwardingTargetForSelector:aSelector];
}

如果我们也没有重写这个方法, 或者没有返回一个对象呢? 上面的tmp/msgsend日志和官方文档中都指出了会调用forwardInvocation方法, 接下来我们看下 forwardInvocation

3. forwardInvocation 消息重定向

首先看下官方文档


图片.png

大致意思就是

当向对象发送没有对应方法的消息时,运行时系统给接收方一个机会将消息委托给另一个接收方。它通过创建一个表示消息的NSInvocation对象并向接收者发送一个包含这个NSInvocation对象作为参数forwardInvocation:消息来委托消息。然后,接收方的forwardInvocation:方法可以选择将消息转发另一个对象。(如果该对象也不能响应消息,那么它将获得一个转发消息的机会。)
因此,forwardInvocation: message允许对象与其他对象建立关系,对于某些消息,这些对象将代表它行事。在某种意义上,转发对象能够“继承”它所转发消息对象的某些特征

还有重要的一点


图片.png

为了响应对象本身不能识别的方法,您必须重写methodSignatureForSelector:forwardInvocation:。转发消息的机制使用methodSignatureForSelector:获得的信息来创建要转发的NSInvocation对象。重写方法必须给定的选择器提供适当的方法签名,可以通过预先构造一个选择器,也可以通过向另一个对象请求一个选择器

看下代码

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    // anInvocation 保存 - 方法
    [anInvocation invoke];
}

打印日志

图片.png

结果: 运行正常无崩溃

你可能感兴趣的:(十一、消息转发机制)