Runtime—消息发送与转发

470 阅读7分钟

开发者在编码时,可以向任意一个对象(包括nil)发送消息,即使消息没有实现,编译也会通过。在编译阶段,只确定了要向接受者发送这条消息,至于接受者将如何响应和处理这条消息,在运行时Runtime负责对象的创建即消息的发送及转发。

  • objc_msgSend介绍
  • 消息发送阶段(Messaging阶段)
  • 消息转发(Message Forwarding阶段)
  • 消息发送及转发流程图
objc_msgSend介绍

Object-C中的【object message】在编译阶段会被转化成objc_msgSend方法:

/*
self:方法接受对象
op:方法选择器
*/
id objc_msgSend(id self, SEL op, ...)
// SEL类型定义
typedef struct objc_selector *SEL;

objc_selector是一个映射到方法的C字符串,Objc中不同类的同一个方法名的方法所对应的方法选择器是一样的,方法名字相同,参数类型不同,方法选择其也是一样的。

objc_msgSend方法做了如下几件事情:

  1. 检查selectors是否是需要忽略的;

  2. 检查target是否是nil

    • 如果是nil,并且有相应的处理nil的函数,则跳转到该函数;
    • 如果是nil,没有对应的处理函数,则清理现场并返回;这也是nil对象调用方法不会崩溃的原因。
  3. 如果target不为nil,则通过对象的isa指针找到对应的类,在类对象中的缓存中查找方法对应的IMP实现。

    如果找到,就跳转进去,如果找不到就在类的方法列表中查找,如果没有,在父类的缓存及方法列表中查找,直到根类NSObject。如果没有就进入消息转发阶段了。

消息发送阶段(Messaging阶段)

源码之下无秘密,下面就从runtime的源码中,来看下方法查找与转发的具体实现,关键入口方法:

// objc-runtime-new.mm
/*
cls:类名
sel:方法选择器
inst:cls或者subclass的实例对象
initialize = NO,尝试避免调用 +initialize(可能会失败)
cache = YES,在加锁的缓存查找之前,会进行不加锁的缓存查找,如果找到则直接返回IMP,提高查找效率;cache = NO,则跳过不加锁查找这一步,直接进行加锁查找,加锁是为了防止在缓存读取时动态添加方法,保证线程安全。
resolver:在继承类中找不到IMP时,是否进行消息解析。
*/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
  1. 在本类的缓存中查找

    // Try this class's cache.
        imp = cache_getImp(cls, sel);
        if (imp) goto done;
    
  2. 在该类的方法列表中查找

    // Try this class's method lists.
        meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    
  3. 在父类中(缓存&方法列表)查找

    // Try superclass caches and method lists.
        curClass = cls;
        // 向上遍历继承关系 直到NSObject.superclass = nil
        while ((curClass = curClass->superclass)) {
            // Superclass cache.
            // 在父类的缓存中查找
            imp = cache_getImp(curClass, sel);
            if (imp) {
              //在继承体系及resolve之后都没有找到IMP,会将sel的IMP设置为_objc_msgForward_impcache放入缓存中(步骤5),imp == (IMP)_objc_msgForward_impcache 标志着没有找到IMP,需要消息转发,所以调用break;终止在父类中的循环查找。
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    // 在父类的缓存中找到IMP后,存到本类的缓存中。问什么存在本类的缓存中呢?因为第一步的查找是本类的缓存开始的,提高查找效率。
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
    
            // Superclass method list.
            // 缓存中没有 到父类方法列表中查找
            meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 找到后放入本类的缓存中
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    
  4. 都没有找到,尝试方法解析一次。

    // No implementation found. Try method resolver once.
        // 4 都没有找到 尝试方法解析一次
        if (resolver  &&  !triedResolver) {
            runtimeLock.unlockRead();
            /*
             方法解析
             */
            _class_resolveMethod(cls, sel, inst);
            // Don't cache the result; we don't hold the lock so it may have 
            // changed already. Re-do the search from scratch instead.
            triedResolver = YES;
            goto retry;
        }
    

    其中_class_resolveMethod方法的具体实现如下:

    void _class_resolveMethod(Class cls, SEL sel, id inst)
    {
      /*
      如果是实例方法会调用 + (BOOL)resolveInstanceMethod:(SEL)sel
      如果是类方法会调用 + (BOOL)resolveClassMethod:(SEL)sel
      如果开发者在resolve方法中动态添加了IMP 在下面的retry即重新走一遍的方法查找流程中 会找到并存入缓存中
      */
        if (! cls->isMetaClass()) {
            // try [cls resolveInstanceMethod:sel]
            _class_resolveInstanceMethod(cls, sel, inst);
        } 
        else {
            // try [nonMetaClass resolveClassMethod:sel]
            // and [cls resolveInstanceMethod:sel]
            _class_resolveClassMethod(cls, sel, inst);
            if (!lookUpImpOrNil(cls, sel, inst, 
                                NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
            {
                _class_resolveInstanceMethod(cls, sel, inst);
            }
        }
    }
    

    _class_resolveInstanceMethod方法的具体实现:

    static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
    {
        if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // Resolver not implemented.
            return;
        }
    
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(cls, SEL_resolveInstanceMethod, 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(cls, sel, inst, 
                                 NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
    
        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));
            }
        }
    }
    

    注意

    • lookUpImpOrNillookUpImpOrForward的实现很像,区别在于当找不到IMP时,lookUpImpOrForward中会将IMP赋值为**_objc_msgForward_impcache**,存入缓存并返回,用于后面的消息转发,体现在步骤5;而lookUpImpOrNil会返回nil,不进行消息转发。
    • _objc_msgForward_impcache,是放在IMP缓存中的一个标记,标记在继承系统以及方法resolve时都没有找到IMP,用于后面的消息转发,在步骤3遍历父类缓存列表时,如果发现sel对应的IMP是_objc_msgForward_impcache,就停止向上父类的查找。

    开发者通过重写resolveInstanceMethod实现方法解析:

    #import "Person.h"
    /*
    Person类声明了- (void)sweepFloor方法,但未实现。
    */
    //自定义方法实现
    void sweepImp (id self,SEL _cmd){
        NSLog(@"sweepImp");
    }
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if ([NSStringFromSelector(sel) isEqualToString:@"sweepFloor"]) {
          //添加方法- (void)sweepFloor的实现为sweepImp
            class_addMethod(self, sel, (IMP)sweepImp, "v@:");
        }
        return [super resolveInstanceMethod:sel];
    }
    /*
    补充介绍:方法参数编码类型
    为了帮助运行时系统,编译器将每个方法的返回参数和传入参数的类型编码为字符串,并将该字符串与方法选择器关联。
    class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 
    class_addMethod的最后一个参数types为方法的返回参数&传入参数类型编码后字符,这里需要开发者手动传入,可以通过@encode(type)获取对应编码字符。
    
    - (void)getTypes {
        char *voidChar = @encode(void);
        char *seletorChar = @encode(SEL);
        char *idChar = @encode(id);
        NSLog(@"参数编码:%s %s %s",voidChar,seletorChar,idChar);//参数编码:v @ :
    }
    
    */
    
  5. 如果在resolve方法中没有IMP 进行消息转发(Message Forwarding)

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    
    // 5 如果在resolve方法中没有IMP,将imp赋值为_objc_msgForward_impcache,存入缓存中,然后进行消息转发(Message Forwarding),用于外部使用的IMP必须转换为_objc_msgForward或者_objc_msgForward_stret
    //_objc_msgForward_impcache为函数指针,存储在方法缓存中。
        imp = (IMP)_objc_msgForward_impcache;
        cache_fill(cls, sel, imp, inst);
    

    至此,消息发送阶段已完成,进入消息转发阶段。

消息转发(Message Forwarding阶段)

在执行_objc_msgForward之后会调用objc_forward_handler函数,objc_forward_handler的值为objc_defaultForwardHandler

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

上面这句打印就是我们经常在xcode崩溃中见到的方法为实现的崩溃日志。

在执行objc_forward_handler之前,开发者可以通过重写类的一下方法,实现消息转发。

  • 为方法指定新的接受者

    #import "Person.h"
    #import "SweepRobot.h"
    //类为Person,声明了sweepFloor(扫地)方法却未实现,转发给扫地机器人(SweepRobot对象)实现
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSLog(@"%s_%@",__FUNCTION__,NSStringFromSelector(aSelector));
        if ([NSStringFromSelector(aSelector) isEqualToString:@"sweepFloor"]) {
            return [SweepRobot new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    
    #import "SweepRobot.h"
    
    @implementation SweepRobot
    - (void)sweepFloor {
        NSLog(@"%@ %s实现",[self class],__func__);
    }
    @end
    
  • 获取方法签名,创建NSInvocation

    @interface Person ()
    @property (nonatomic,strong)SweepRobot *robot;
    @end
      
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSLog(@"%s",__FUNCTION__);
        return [self.robot methodSignatureForSelector:aSelector];
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"%s anInvocation:%@",__FUNCTION__,anInvocation);
        [anInvocation invokeWithTarget:self.robot];
    }
    
    - (SweepRobot *)robot {
        if (!_robot) {
            _robot = [SweepRobot new];
        }
        return _robot;
    }
    
消息发送及转发流程图