美文网首页
iOS 底层动态方法决议 & 消息转发

iOS 底层动态方法决议 & 消息转发

作者: Mr木子李 | 来源:发表于2022-05-09 20:28 被阅读0次

    前言

    • OC调用方法,底层是调用 objc_msgSend 发送消息。在发送消息时会经过一系列的快速 查找、慢速查找,如果查找到对应的 IMP,直接返回;如果没有找到,就会进入到方法的动态方法决议和消息转发流程。

    • 这篇文章就是深入探索动态方法决议消息转发

    一 、 动态方法决议

    接着上一篇,在慢速查找流程未找到方法实现时,首先会尝试一次动态方法决议:

        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            //动态方法决议的控制条件
            behavior ^= LOOKUP_RESOLVER; 
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    
    

    进入动态方法决议阶段,源码如下

    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); //元类执行解析方式
    // -- 如果resolveClassMethod找到了,就不会走这里了
    //-- 如果没找到,这个if必然会走,之前调用lookUpImpOrForward,已经给该sel的方法缓存了imp = forward_imp
    // -- 必然会走到done_nolock,返回一个nil
    // -- 类方法在元类中也是以实例方法的形式存在,所以还需再走一遍实例方法的动态方法决议流程
            if (!lookUpImpOrNil(inst, sel, cls)) {
                resolveInstanceMethod(inst, sel, cls);//类执行解析方式
            }
        }
    //重新进入 lookUpImpOrForward  根据 behavior | LOOKUP_CACHE进行判断执行方式
        return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
    }
    
    

    在resolveInstanceMethod方法中对实例方法动态解析,源码如下:

    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        SEL resolve_sel = @selector(resolveInstanceMethod:);
        // 1\. 判断系统是否实现SEL_resolveInstanceMethod方法
        // 即+(BOOL)resolveInstanceMethod:(SEL)sel, 
        // 继承自NSObject的类,默认实现,返回NO
        if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) 
        {
            // Resolver not implemented.
            // 不是NSObject的子类,也未实现+(BOOL)resolveInstanceMethod:(SEL)sel,
            // 直接返回,没有动态解析的必要
            return;
        }
    
        // 2\. 系统给你一次机会 - 你要不要针对 sel 来操作一下下
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(cls, resolve_sel, sel);
        // 3\.-- 再去查一次imp,如果在cls的继承链上自定义实现了`resolveInstanceMethod`方法并在里面添加了imp,就可以找到imp
        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));
            }
        }
    }
    
    

    在resolveClassMethod方法中对实例方法动态解析,源码如下:

    static void resolveClassMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        ASSERT(cls->isMetaClass());
    //: -- 容错处理,判断该元类的继承链中是否有resolveClassMethod方法
    //: -- 如果自定义没实现,则会找到NSObject
        if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
            // Resolver not implemented.
            return;
        }
    
        Class nonmeta;
        {
            mutex_locker_t lock(runtimeLock);
    //: -- 得到元类/类的对应的类
    //: -- 因为我们只能在类里实现resolveClassMethod方法,无法去元类实现,所以这里把消息接受者设置为当前类
            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);
            }
        }
    //: -- 发送消息,调用nonmeta中的`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,如果上面调用用nonmeta中的`resolveClassMethod `方法里面给元类添加了imp,就会直接找到
        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 resolveClassMethod:%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));
            }
        }
    }
    
    
    • 在经过resolveMethod_locked方法后,进行resolveInstanceMethod,重新进行一遍lookUpImpOrForward;即: 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程

    • 使用动态方法决议流程:

      image
    • 使用动态方法决议,代码举例:

    //对象使用
    + (BOOL)resolveInstanceMethod:(SEL)sel{
    
        if (sel == @selector(say666)) {
            NSLog(@"%@ 来了",NSStringFromSelector(sel));
      /**
             添加方法
    
             @param self 调用该方法的对象
             @param sel 方法的名
             @param IMP 新添加的方法,是c语言实现的
             @param type :方法签名, 新添加的方法的类型,包含函数的返回值以及参数内容类型,eg:void xxx(NSString *name, int size),类型为:v@i
             */
            IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
            Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
            const char *type  = method_getTypeEncoding(sayMMethod);
            return class_addMethod(self, sel, imp, type);
        }
    
        return [super resolveInstanceMethod:sel];
    }
    
    //类使用
    + (BOOL)resolveClassMethod:(SEL)sel{
        NSLog(@"%@ 来了",NSStringFromSelector(sel));
        if (sel == @selector(sayNB)) {
    
            IMP imp           = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
            Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
            const char *type  = method_getTypeEncoding(sayMMethod);
            return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
        }
        return [super resolveClassMethod:sel];
    }
    
    就算没有实现方法say666 ,但是class_addMethod,依然不会崩溃。
    
    

    优化:根据resolveMethod_locked的流程图,如果是元类调用,最后还是会走实例方法,根据isa走位图,可知元类继承链,最终继承NSObject,所以我们可以给NSObject 写个分类,直接在分类中综合两个方法如下:

    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(say666)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
    
            IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
            Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
            const char *type = method_getTypeEncoding(sayMethod);
            return class_addMethod(self, sel, imp, type);
        }else if (sel == @selector(sayNB)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
    
            IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
            Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
            const char *type = method_getTypeEncoding(lgClassMethod);
            return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
        }
        return NO;
    }
    
    

    风险:使用这方法的风险可能造成其他SDK 或者系统方法也使用了类似的方法,造成冲突。所以我们使用的类名LGPerson一定要特殊唯一。

    注 :
    使用这种办法的前提是:相关方法代码已经实现,只是在运行时将改方法动态添加到目标类中。。

    二、 消息转发机制

    在方法查找过程中,经过缓存查找,方法列表查找和动态方法解析,如果经历慢速查找都没有查找到IMP,也没有进行方法动态解析,那么我们还有办法进行方法实现: 消息转发机制。但是我们只知道有消息转发这个机制,但是我们始终找不到 消息转发的实现方法和实现。这个时候:

    开启凡人的视角:

    • lldb方式断点查看: 通过进入 [person sayHello]之后的断点查看在 class 为 LGPerson的时候 Sel ,可以发现forwardingTargetForSelectormethodSignatureForSelector。如图:
    image

    开启上帝视角:

    • 方式1:通过instrumentObjcMessageSends方式打印发送消息的日志

    • 方式2:通过hopper/IDA反编译

    方式1 :instrumentObjcMessageSends

    通过lookUpImpOrForward --> log_and_fill_cache--> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现如下:

    /***********************************************************************
    * instrumentObjcMessageSends
    **********************************************************************/
    // Define this everywhere even if it isn't used to simplify fork() safety code.
    spinlock_t objcMsgLogLock;
    #if !SUPPORT_MESSAGE_LOGGING
    
    ///开启instrumentObjcMessageSends
    void    instrumentObjcMessageSends(BOOL flag)
    {
    }
    #else
    
    ///将方法执行记录通过打印出来
    bool objcMsgLogEnabled = false;
    static int objcMsgLogFD = -1;
    bool logMessageSend(bool isClassMethod,
                        const char *objectsClass,
                        const char *implementingClass,
                        SEL selector)
    {
     .........
    }
    ///结束instrumentObjcMessageSends
    void instrumentObjcMessageSends(BOOL flag)
    {
     .........
    // SUPPORT_MESSAGE_LOGGING
    #endif
    
    

    所以,在main中调用instrumentObjcMessageSends打印方法调用的日志信息犹如上面的格式:

    • 1、在main中通过extern 声明instrumentObjcMessageSends方法
    • 2、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES

    不带源码的工程中这样实现:

    ///extern 修饰 我们可以拿来调用 苹果提供了打印方法流程的方法 
    extern void instrumentObjcMessageSends(BOOL flag);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            LGPerson *person = [LGPerson alloc];
    //开启
            instrumentObjcMessageSends(YES);
    //去logMessageSend找打印信息
            [person sayHello];
    //关闭
            instrumentObjcMessageSends(NO);
        }
        return 0;
    }
    
    

    我们在objc4源码工程再去找logMessageSend方法:

    /***********************************************************************
    * logMessageSend
    **********************************************************************/
    bool objcMsgLogEnabled = false;
    static int objcMsgLogFD = -1;
    
    bool logMessageSend(bool isClassMethod,
                        const char *objectsClass,
                        const char *implementingClass,
                        SEL selector)
    {
        char    buf[ 1024 ];
    
        // Create/open the log file
        if (objcMsgLogFD == (-1))
        {
            snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
            objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
            if (objcMsgLogFD < 0) {
                // no log file - disable logging
                objcMsgLogEnabled = false;
                objcMsgLogFD = -1;
                return true;
            }
        }
    
        // Make the log entry
        ........
        return false;
    }
    
    

    运行不带源码的工程,并前往/tmp/msgSends 目录,发现有msgSends开头的日志文件;
    快捷键:command + shift + g ---> 输入 /tmp/msgSends -->回车
    打开msgSends.文件:

    image

    可以看出 ,崩溃前执行了
    两次动态方法决议:resolveInstanceMethod方法
    两次消息快速转发:forwardingTargetForSelector方法
    两次消息慢速转发:methodSignatureForSelector + resolveInstanceMethod

    方式2:通过hopper/IDA反编译找到forwardingTargetForSelector等转发方法的实现

    • Hopper和IDA是一个可以帮助我们静态分析可视性文件的工具,可以将可执行文件反汇编成伪代码、控制流程图等,下面以mac端Hopper为例(Hopper :要收费的,IDA:是Windows端的)。

    • 通过打印崩溃的堆栈信息进行查看崩溃信息如下:

    image

    通过路径/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 获取到CoreFoundation的可执行文件:

    image
    • 通过 hopper 进行反编译,步骤如下:
      打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)如图:
    image

    next:先搜索__forwarding_prep_0___

    image

    跳转至:___forwarding___

    image
    • 终于找到了forwardingTargetForSelector:方法了:咋们再来看看 其执行流程,简单分析如下:

      image

    开启上帝视角之后,我们通过汇编分析,可以清晰看到转发流程方法:

    【快速转发】forwardingTargetForSelector
    【慢速转发】methodSignatureForSelector —>forwardInvocation

    注:forwardInvocation :消息重定向,以后再分析。

    动态方法决议和消息转发整体的流程如下:

    image

    注: 不论是否进行动态决议都会重新lookUpImpOrForward,这里会有behavior的判断,是否经历过动态决议处理。所以开始有一次动态决议处理方法的机会。

    三、 实际举例验证:

    image
    image

    注:如果不进行快速消息转发 也不进行慢速转发就会崩溃,可以试试。

    四、 总结

      1. 消息转发机制是在汇编中实现的,并且属于CoreFoundation框架中,不开源的。我们可以通过反汇编的方式去查看;
      1. 在我们没有实现方法的时候,慢速查找也找不到方法的时候,我们有三次机会去实现方法:
      • 1). 动态方法决议:实现resolveInstanceMethod
      • 2). 快速消息转发:实现forwardingTargetForSelector
      • 3). 慢速消息转发:实现methodSignatureForSelector —>forwardInvocation
      1. 如果objc_msgSend快速查找和慢速查找失败,未实现动态方法决议和消息转发,则程序直接报错崩溃unrecognized selector sent to instance

    五、 拓展

      1. objc_msgForward_impcache 的转换 ;

    objc_msgForward_impcache调用

    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
        const IMP forward_imp = (IMP)_objc_msgForward_impcache;
        IMP imp = nil;
        Class curClass;
    ......
    
    

    _objc_msgForward_impcache源码:

    STATIC_ENTRY __objc_msgForward_impcache
        // No stret specialization.
        b   __objc_msgForward
        END_ENTRY __objc_msgForward_impcache
    
        ENTRY __objc_msgForward
        adrp    x17, __objc_forward_handler@PAGE
        ldr p17, [x17, __objc_forward_handler@PAGEOFF]
        TailCallFunctionPointer x17
        END_ENTRY __objc_msgForward
    
    

    _objc_msgForward_impcache 只是个内部的函数指针,只存储于类的方法缓存中,需要被转化为_objc_msgForward 才能被外部调用。

    相关文章

      网友评论

          本文标题:iOS 底层动态方法决议 & 消息转发

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