美文网首页底层原理
objc_msgSend 消息转发流程探究三

objc_msgSend 消息转发流程探究三

作者: 晨曦的简书 | 来源:发表于2021-07-06 13:52 被阅读0次

    方法找不到的报错底层原理

    我们在前面 objc_msgSend 消息转发流程探究二 中最后讲到,当在缓存跟方法列表中都找不到对应的 imp 的时候,会把 imp 赋值为 forward_imp 并返回出去。

    这里我们全局搜索 __objc_msgForward_impcache 会看到汇编代码的执行,b __objc_msgForward

    接着我们再搜索 __objc_msgForward,进到 __objc_msgForward 方法后最后会执行 TailCallFunctionPointer 方法。

    这里我们可以看到 TailCallFunctionPointer 的宏定义就是跳转到 $0,这里 $0 就是 TailCallFunctionPointer x17 传进来的参数 x17x17 就是 __objc_forward_handler

    这里我们再搜索 __objc_forward_handler 会找不到,因为这里执行的不再是汇编代码,我们去掉 __ 继续搜索。这里就会来到 objc_defaultForwardHandler 方法。这里会打印报错信息,打印结果就是 + , - 前缀[类名 方法名]: unrecognized selector sent to instance 对象地址 " "(no message forward handler is installed)"

    对象方法动态决议

    上面我们讲到,当 imp 查找不到的时候会来到 objc_defaultForwardHandler 方法,会打印崩溃日志,也就是调用一个没有实现的方法的时候系统会崩溃,但是在调用 objc_defaultForwardHandler 方法之前系统还给了我们一次动态决议的机会,我们可以在这里做一些处理。下面是源码的执行流程。

    1. if (slowpath(behavior & LOOKUP_RESOLVER))
    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
    
        // No implementation found. Try method resolver once.
        // 这个方法是单例方法,只会执行一次
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    }
    

    就是执行 objc_defaultForwardHandler 方法之前会执行 lookUpImpOrForward 方法中的这段代码。这里 behaviorlookUpImpOrForward 方法的参数,全局这里我们全局搜索可以看到 behavior 为 3,LOOKUP_RESOLVER = 2,所以 behavior & LOOKUP_RESOLVER = 3 & 2 = 2behavior ^= LOOKUP_RESOLVER 等于 behavior = 2 ^ 2 = 0, 最后再执行 slowpath(behavior & LOOKUP_RESOLVER) 因为behavior为 0,0 与上任何数都为 0,所以不会再进到这个判断了。进到判断里面会执行 resolveMethod_locked 方法。

    enum {
        LOOKUP_INITIALIZE = 1,
        LOOKUP_RESOLVER = 2,
        LOOKUP_NIL = 4,
        LOOKUP_NOCACHE = 8,
    };
    
    1. resolveMethod_locked
    resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
    {
        runtimeLock.assertLocked();
        ASSERT(cls->isRealized());
    
        runtimeLock.unlock();
    
        //这段代码的意义是当我们找出 imp 的时候,会先在缓存跟方法列表中查找,都找不到的会一般会崩溃,但是如果直接崩溃的话会导致系统不稳定,所以这里苹果的开发者会再次给我一次机会,只要我们在 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior) 之前处理了,系统会再次执行 lookUpImpOrForwardTryCache 方法,再次查找缓存跟方法列表
        // 这里会有判断如果不是元类会执行 resolveInstanceMethod 方法,如果是元类就会执行 resolveClassMethod 方法。
        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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
        }
    
        // chances are that calling the resolver have populated the cache
        // so attempt using it
        return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
    }
    

    这段代码的意义是当我们找出 imp 的时候,会先在缓存跟方法列表中查找,都找不到的会一般会崩溃,但是如果直接崩溃的话会导致系统不稳定,所以这里苹果的开发者会再次给我一次机会,只要我们在 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior) 之前处理了,系统会再次执行 lookUpImpOrForwardTryCache 方法,再次查找缓存跟方法列表
    // 这里会有判断如果不是元类会执行 resolveInstanceMethod方法,如果是元类就会执行 resolveClassMethod 方法。这里我们先来看 resolveInstanceMethod 方法。

    1. resolveInstanceMethod
    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        SEL resolve_sel = @selector(resolveInstanceMethod:);
    
        if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
            // Resolver not implemented.
            return;
        }
    
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        // 在这里系统会自动给当前的类发送 resolve_sel 消息,也就是 resolveInstanceMethod 方法。
        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
        // 如果在当前类有执行 resolve_sel 并且有做处理,接着就会在当前表里面继续查找一遍
        IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    }
    

    在这个方法类名系统会主动给当前的类发送 resolve_sel 消息,如果在当前类有执行 resolve_sel 并且有做处理,接着就会在当前表里面继续查找一遍。执行 lookUpImpOrNilTryCache 方法。

    这里我们通过案例来看一下。

    @interface LGPerson : NSObject
    
    - (void)say1;
    
    @end
    
    
    #import "LGPerson.h"
    
    @implementation LGPerson
    
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSLog(@"%s",__func__);
        return YES;
    }
    
    
    @end
    

    我们定义一个 LGPerson 类,在 .h 文件中声明 say1 方法,而 .m 文件只是重写了 resolveInstanceMethod 方法并且打印函数信息。

    这里我们可以看到在日志输入的第三行打印了报错信息,但是在这之前调用了 resolveInstanceMethod 方法。那么我们是不是在 resolveInstanceMethod 方法中做些处理,是不是就可以方法崩溃呢?

    #import "LGPerson.h"
    #import <objc/message.h>
    
    @implementation LGPerson
    
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(say1)) {
            // 指向 sayHello 方法的 imp 指针
            IMP sayHelloImp = class_getMethodImplementation(self, @selector(sayHello));
            // method
            Method method = class_getInstanceMethod(self, @selector(sayHello));
            // 类型
            const char *type = method_getTypeEncoding(method);
            // 这里对 say1 方法添加新的 imp
            return class_addMethod(self, sel, sayHelloImp, type);
        }
        return YES;
    }
    
    @end
    

    这里我们在 resolveInstanceMethod 方法中把 say1 方法对应的 sel 添加了对应 sayHello 方法的 imp,当我们再次调用 say1 方法的时候会看到没有报错,而是执行了 sayHello 方法。

    类方法的动态决议

    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 方法。
            resolveClassMethod(inst, sel, cls);
            if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
        }
        return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
    }
    

    我们在前面 resolveMethod_locked 方法中讲到,如果是元类就会执行 resolveClassMethod 方法,这里我们就来看一下类方法的动态决议流程。

    1. resolveClassMethod
    static void resolveClassMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        ASSERT(cls->isMetaClass());
    
        // 判断 resolveClassMethod 方法是否存在
        if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
            // Resolver not implemented.
            return;
        }
    
        // 因为类方法存在于元类里面,所以这里对原理进行局部操作,防止方法没有实现
        Class nonmeta;
        {
            mutex_locker_t lock(runtimeLock);
            nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
            // +initialize path should have realized nonmeta already
            if (!nonmeta->isRealized()) {
                _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                            nonmeta->nameForLogging(), nonmeta);
            }
        }
        
        // 在这里会对元类发送 resolveClassMethod 消息,那么这里我们想做拦截改怎么拦截呢?这里是对元类发送消息,因为元类的方法是以对象方法存在,我们都知道类的类方法以对象方法的形式存在于元类里面,又因为 resolveClassMethod 为类方法,所以我们在类里面重写 resolveClassMethod 方法就可以拦截
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    
        // Cache the result (good or bad) so the resolver doesn't fire next time.
        // +resolveClassMethod adds to self->ISA() a.k.a. cls
        IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    }
    

    在这里会对元类发送 resolveClassMethod 消息,那么这里我们想做拦截改怎么拦截呢?这里是对元类发送消息,因为元类的方法是以对象方法存在,我们都知道类的类方法以对象方法的形式存在于元类里面,又因为resolveClassMethod 为类方法,所以我们在类里面重写 resolveClassMethod 方法就可以拦截。这里我们通过案例来试一下。

    @interface LGPerson : NSObject
    
    - (void)say1;
    
    + (void)say2;
    
    @end
    
    
    #import "LGPerson.h"
    #import <objc/message.h>
    
    @implementation LGPerson
    
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    
    + (void)sayHappy{
        NSLog(@"%s",__func__);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(say1)) {
            // 指向 sayHello 方法的 imp 指针
            IMP sayHelloImp = class_getMethodImplementation(self, @selector(sayHello));
            // method
            Method method = class_getInstanceMethod(self, @selector(sayHello));
            // 类型
            const char *type = method_getTypeEncoding(method);
            // 这里对 say1 方法添加新的 imp
            return class_addMethod(self, sel, sayHelloImp, type);
        }
        return YES;
    }
    
    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector(say2)) {
            // 指向 sayHappy 方法的 imp 指针
            IMP sayHappyImp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayHappy));
            // method
            Method method = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayHappy));
            // 类型
            const char *type = method_getTypeEncoding(method);
            // 这里对 say2 方法添加新的 imp
            return class_addMethod(objc_getMetaClass("LGPerson"), sel, sayHappyImp, type);
        }
        return YES;
    }
    
    @end
    

    跟对象方法动态决议类似,我们重写 resolveClassMethod 方法,并判断 selsay2 方法的时候,设置 imp 为指向 sayHappy 方法的 sayHappyImp,最后我们调用 say2 方法的时候调用了 sayHappy 方法。
    resolveClassMethod(inst, sel, cls);
            if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);
            }
    

    这里我们可以发现与对象方法动态决议有区别的地方,这里还会执行一次 resolveInstanceMethod 方法,这是因为类方法在元类中是以对象方法存在,所以也就会走对象方法的决议流程。所以 resolveInstanceMethod 方法会被调用两次。

    #import "NSObject+LG.h"
    #import <objc/message.h>
    
    @implementation NSObject (LG)
    
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    
    + (void)sayHappy{
        NSLog(@"%s",__func__);
    }
    
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(say1)) {
            // 指向 sayHello 方法的 imp 指针
            IMP sayHelloImp = class_getMethodImplementation(self, @selector(sayHello));
            // method
            Method method = class_getInstanceMethod(self, @selector(sayHello));
            // 类型
            const char *type = method_getTypeEncoding(method);
            // 这里对 say1 方法添加新的 imp
            return class_addMethod(self, sel, sayHelloImp, type);
        } else if (sel == @selector(say2)) {
            // 指向 sayHappy 方法的 imp 指针
            IMP sayHappyImp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayHappy));
            // method
            Method method = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayHappy));
            // 类型
            const char *type = method_getTypeEncoding(method);
            // 这里对 say1 方法添加新的 imp
            return class_addMethod(objc_getMetaClass("LGPerson"), sel, sayHappyImp, type);
        }
        return NO;
    }
    
    @end
    

    所以我们在 NSObject+LG 分类里面,只重写 resolveInstanceMethod 方法,并统一在这里处理也是可以的。

    经过上面对前面动态方法的探究,我们会思考一个问题,苹果给我们提供动态决议方法的意义是什么呢?我们总结如下。

    1. 通过给 NSObject 添加分类的形式,能全局对所有找不到的方法进行监听。
    2. 可以针对自己的模块针对自己定义的方法进行监控,防止崩溃并做些其他处理,同时通过后台上传错误日志,及时对 bug 进行修改。

    相关文章

      网友评论

        本文标题:objc_msgSend 消息转发流程探究三

        本文链接:https://www.haomeiwen.com/subject/wcpbultx.html