【OC Runtime】消息发送机制,动态方法解析,消息转发机制

目录

弄明白对象、类是怎么调用方法的就行
一、消息发送机制objc_msgSend
二、动态方法解析resolveMethod
三、消息转发机制objc_msgForward
四、程序崩掉unrecognized selector sent to instance
五、super关键字objc_msgSendSuper


一、消息发送机制objc_msgSend


消息发送机制是指,OC对象调用方法[object methodName],都会转换为objc_msgSend(object, @selector(methodName))函数的调用——即给一个对象发送一个消息,这个转换过程是在编译时就完成的,而具体怎么给对象发送消息则是在运行时完成的。objc_msgSend函数内部具体怎么给对象发送消息的呢?(消息发送流程、方法调用流程)

先总结在这里:

  • 一进入objc_msgSend函数,系统会首先判断消息接收者是不是nil,如果是nil直接return,结束该方法的调用,程序并不会崩掉。

  • 如果不是nil,则根据对象的isa指针找到该对象所属的类,去这个类的方法缓存——cache里查找方法,方法缓存是通过散列表实现的,所以查找效率非常高,如果找到了就直接调用,如果没有找到,则会去类的方法列表——methods里查找,这里对于已排过序的方法列表采用二分查找,对于未排过序的方法列表则采用遍历查找,如果在类的方法列表找到了方法,则首先把该方法缓存到当前类的cache中,然后调用该方法,如果没有找到,则会根据当前类的superclass指针找到它的父类,去父类里查找。

  • 找到父类后,会首先去父类的方法缓存——cache里查找方法,如果找到了,则首先把该方法缓存到当前类的cache中(注意不是父类的cache哦),然后调用该方法,如果没有找到,则会去父类的方法列表——methods里查找。如果在父类的方法列表找到了方法,则首先把该方法缓存到当前类的cache中(注意不是父类的cache哦),然后调用该方法,如果没有找到,则会一层一层往上,直到根类,直到nil

  • 如果到了nil,还是没有找到方法,就会触发动态方法解析。

从整个消息发送流程,我们也感受到:消息发送关注的仅仅是消息接收者和SEL,无非就是通过消息接收者的isa指针和superclass指针去找SEL嘛,根本就没有什么绝对的标识来表明某个方法是实例方法还是类方法,所以如果出现类调用实例方法也不要惊讶哦,比如[NSObject -test]是没有问题的,你只要抓紧方法调用流程这条线就可以了。

  • objc-msg-arm64.s文件,汇编代码(伪代码)
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
*
********************************************************************/

    ENTRY _objc_msgSend // _objc_msgSend函数的入口

    /*
     cmp:比较。
     p0:可以看做是_objc_msgSend函数的第一个参数——即消息接收者object。
     #0:可以看做是nil。
     */
    cmp    p0, #0 // 判断消息接收者是不是nil

    /*
     b:跳转。
     le:跳转的条件,小于等于。
     LNilOrTagged:要跳转到的地方。
     */
    b.le    LNilOrTagged // 如果消息接收者为nil,则跳转到LNilOrTagged执行,如果消息接收者不为nil,则继续往下执行

    ldr    p13, [x0] // p13 = isa,从寄存器中获取该对象的isa共用体
    and    p16, p13, #ISA_MASK // p16 = class,isa & ISA_MASK,获取到该对象所属类的内存地址,这就算找到该对象所属的类了

LGetIsaDone:
    CacheLookup NORMAL // 去这个类的方法缓存里查找方法

LNilOrTagged:
    ret // 直接return

    END_ENTRY _objc_msgSend // _objc_msgSend函数的出口

可见一进入objc_msgSend函数,系统会首先判断消息接收者是不是nil,如果是nil直接return,所以给nil发送一个消息程序并不会崩溃;如果不是nil,则根据对象的isa指针找到该对象所属的类,去这个类的方法缓存里查找方法。

/********************************************************************
*
* CacheLookup、CacheHit、CheckMiss
*
********************************************************************/

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // 命中,即在方法缓存里找到了方法
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // 未命中,即在方法缓存里没有找到方法
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop
    
.endmacro


.macro CacheHit

    TailCallCachedImp x17, x12, x1 // 验证并直接调用IMP指向的函数

.endmacro


.macro CheckMiss

    __objc_msgSend_uncached // 执行__objc_msgSend_uncached函数

.endmacro


/********************************************************************
*
* __objc_msgSend_uncached、MethodTableLookup
*
********************************************************************/

STATIC_ENTRY __objc_msgSend_uncached

MethodTableLookup // 查找方法列表

END_ENTRY __objc_msgSend_uncached


.macro MethodTableLookup

    bl    __class_lookupMethodAndLoadCache3 // 跳转执行__class_lookupMethodAndLoadCache3这个C函数,并要求一个返回值,收到返回值IMP后,会执行它所指向的函数

.endmacro

你看上面一堆代码,注释里又是散列表,又是SELmask,又是散列算法的,就知道它们是在方法缓存里查找方法,如果找到了就直接调用,如果没有找到,则会去类的方法列表里查找方法。

  • objc-runtime-new.mm文件,C/C++代码(伪代码)
/********************************************************************
*
* _class_lookupMethodAndLoadCache
*
********************************************************************/

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls/*当前对象所属的类*/, sel/*消息*/, obj/*当前对象*/,
                              YES/*initialize*/, NO/*标识在cache没有找到方法*/, YES/*resolver*/);
}

汇编在调用这个C函数的时候,默认会传递几个参数过来。

/********************************************************************
*
* lookUpImpOrForward
*
********************************************************************/

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

 retry:
    // 在当前类的方法列表里查找方法(汇编部分已经在当前类的方法缓存里查找过了)
    {
        Method meth = getMethodNoSuper_nolock(cls/*当前类*/, sel/*消息*/);
        if (meth) { // 如果找到了方法
            // 首先把该方法缓存到当前类的cache中
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done; // 然后跳转到done
        }
    }

    // 在当前类父类的方法缓存和方法列表里查找方法,并一层一层往上,直到根类,直到nil
    {
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass) // for循环的作用是一层一层往上,直到根类,直到nil
        {
            // 在父类的方法缓存里查找方法
            imp = cache_getImp(curClass, sel);
            if (imp) { // 如果找到了方法
                // 首先把该方法缓存到当前类的cache中(注意不是父类的cache哦)
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done; // 跳转到done
            }
            
            // 在父类的方法列表里查找方法
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) { // 如果找到了方法
                // 首先把该方法缓存到当前类的cache中(注意不是父类的cache哦)
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done; // 跳转到done
            }
        }
    }

    // 如果正常的消息发送流程走完,没有找到方法,就会触发动态方法解析
    if (!triedResolver) { // 首先判断之前有没有进行过动态方法解析,有则直接触发消息转发机制,没有则进行动态方法解析
        
        resolveMethod(cls, sel, inst); // 进行动态方法解析

        // 动态方法解析完成后
        triedResolver = YES; // 标记为已经进行过动态方法解析
        goto retry; // 并重新走一遍消息发送流程来查找方法
    }

    // 如果正常的消息发送流程和动态方法解析都走完,还是没有找到方法,就会触发消息转发机制
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    return imp; // 返回方法的IMP,就返回到了汇编那里,汇编收到返回值IMP后,会执行它所指向的函数
}

如果在当前类的方法列表里找到了方法,则首先把该方法缓存到当前类的cache中,然后调用该方法。

如果在当前类的方法列表里还是没有找到方法,则会根据当前类的superclass指针找到它的父类,去父类的方法缓存里查找方法,如果找到了方法,则首先把该方法缓存到当前类的cache中(注意不是父类的cache哦),然后调用该方法。如果在父类的方法缓存里也没有找到方法,则会去父类的方法列表里查找方法,如果找到了方法,则首先把该方法缓存到当前类的cache中(注意不是父类的cache哦),然后调用该方法。如果在父类的方法列表也没有找到方法,则会一层一层往上,直到根类,直到nil

如果到了nil还是没有找到方法,则会触发动态方法解析。

/********************************************************************
*
* getMethodNoSuper_nolock、search_method_list:在类的方法列表里查找方法
*
********************************************************************/

method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
{
    for (auto mlists = cls->data()->methods.beginLists(),
              end = cls->data()->methods.endLists();
         mlists != end;
         ++mlists) // 根据类的methods成员变量,找到所有的方法列表,然后遍历这些方法列表来查找方法
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        // 对于排过序的方法列表,使用二分查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 对于未排序的方法列表,使用普通遍历查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

    return nil;
}

前面我们在讲方法缓存的时候,已经说过方法缓存是通过散列表实现的,它的查找效率非常高。那类的方法列表呢,它是怎么查找方法的?当我们找到类的时候,会根据它的methods成员变量,找到所有的方法列表(包括分类的,分类的方法列表排在前面),然后遍历这些方法列表来查找方法,对于排过序的方法列表,使用二分查找,对于未排序的方法列表,使用普通遍历查找。


二、动态方法解析resolveMethod


如果正常的消息发送流程走完,没有找到方法,就会触发动态方法解析。动态方法解析是指,如果我们在编译时没有为某个方法提供实现,可以在运行时通过类的+resolveInstanceMethod:方法或+resolveClassMethod:方法动态地为这个方法添加实现。

  • 一触发动态方法解析,系统如果发现是没有找到实例方法,就会调用该类的+resolveInstanceMethod:方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到类的methods里;如果发现是没有找到类方法,就会调用该类的+resolveClassMethod:方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到元类的methods里。

  • 动态方法解析完成后,会重新走一遍消息发送流程来查找方法。

  • 如果动态方法解析也没有解决问题,就会触发消息转发机制。

/********************************************************************
*
* resolveMethod
*
********************************************************************/

void resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) { // 如果这个类不是元类——即没找到实例方法
        resolveInstanceMethod(cls, sel, inst);
    }
    else { // 如果这个类是元类——即没找到类方法
        resolveClassMethod(cls, sel, inst)
    }
}


/********************************************************************
*
* resolveInstanceMethod、resolveClassMethod
*
********************************************************************/

void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 首先判断该类有没有实现+resolveInstanceMethod:方法,因为它是个类方法,所以传的是cls->ISA(),没有的话直接return
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO, YES, NO))
    {
        return;
    }

    // 调用该类的+resolveInstanceMethod:方法,们可以在这个方法里动态地为没找到的方法添加实现,会添加到类的methods里
    objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
}

void resolveClassMethod(Class cls, SEL sel, id inst)
{
    // 此处的cls已经是metaClass了,所以直接传的是cls
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, NO, YES, NO))
    {
        return;
    }
    
    // 调用该类的+resolveClassMethod:方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到元类的methods里
    objc_msgSend(nonmeta, SEL_resolveClassMethod, sel);
}

举个例子:

-----------main.m-----------

#import 
#import "INEPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        INEPerson *person;
        [person eat]; // 我们没有实现-eat方法,所以会触发动态方法解析
        
        [INEPerson drink]; // 我们没有实现+drink方法,所以会触发动态方法解析
    }
    return 0;
}
-----------INEPerson.h-----------

#import 

@interface INEPerson : NSObject

- (void)eat;
+ (void)drink;

@end


-----------INEPerson.m-----------

#import "INEPerson.h"
#import 

@implementation INEPerson

// 动态方法解析,处理没找到的实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    // 动态地为这个方法添加实现
    if (sel == @selector(eat)) { // 如果是eat方法再添加,别把别的方法也给写了
        
        Method tempMethod = class_getInstanceMethod(self, @selector(otherEat));
        
        // self:eat方法要添加到当前类的methods里,此处self就是当前类
        // sel:为@selector(eat)添加对应的IMP,即为eat方法添加实现
        // imp、types:我们假设要添加为otherEat方法的IMP和types
        class_addMethod(self, sel, method_getImplementation(tempMethod), method_getTypeEncoding(tempMethod));
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

// 动态方法解析,处理没找到的类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    
    if (sel == @selector(drink)) {
        
        Method tempMethod = class_getClassMethod(self, @selector(otherDrink));
        
        // object_getClass(self):drink方法要添加到当前类元类的methods里,所以此处是object_getClass(self)
        class_addMethod(object_getClass(self), sel, method_getImplementation(tempMethod), method_getTypeEncoding(tempMethod));
        return YES;
    }
        
    return [super resolveClassMethod:sel];
}


#pragma mark - other method

- (void)otherEat {
    
    NSLog(@"INEPerson otherEat");
}

+ (void)otherDrink {
    
    NSLog(@"INEPerson otherDrink");
}

@end

控制台打印:

INEPerson otherEat
INEPerson otherDrink


三、消息转发机制objc_msgForward


如果正常的消息发送流程和动态方法解析都走完,还是没有找到方法,就会触发消息转发机制。消息转发机制是指,把消息转发给别的对象,让别的对象来调用这个方法,因为到了这一步,就表明你这个类本身已经没有能力调用这个方法了,交给别人吧。消息转发又可以分为直接消息转发和完整消息转发。(消息转发机制的源码是不开源的)

  • 直接消息转发

直接消息转发是指,系统会调用该类的forwardingTargetForSelector:方法,我们可以在这个方法里直接把消息转发给别的对象。

举个例子:

-----------main.m-----------

#import 
#import "INEPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        INEPerson *person = [[INEPerson alloc] init];
        [person eat]; // 我们没有实现-eat方法,动态方法解析也没做处理,会触发消息转发机制
        
        [INEPerson drink]; // 我们没有实现+drink方法,动态方法解析也没做处理,会触发消息转发机制
    }
    return 0;
}
-----------INEPerson.h-----------

#import 

@interface INEPerson : NSObject

- (void)eat;
+ (void)drink;

@end


-----------INEPerson.m-----------

#import "INEPerson.h"
#import 

@implementation INEPerson

/**
 * 把消息转发给别的对象
 *
 * @param aSelector 要把哪个方法转发给别的对象——即没找到的方法
 *
 * @return 要把消息转发给哪个对象——即你觉得能调用该方法的对象
 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(eat)) {
        
        // 转发给Student对象,因为它能处理这个方法。我们猜测这里的底层实现,无非就是拿这个返回的对象调用它相应的方法,即objc_msgSend([[INEStudent alloc] init], aSelector)
        return [[INEStudent alloc] init];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

// 注意:处理类方法时,前面要换成“+”
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(drink)) {
        
        return [INEStudent class];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end
-----------INEStudent.h-----------

#import 

@interface INEStudent : NSObject

- (void)eat;
+ (void)drink;

@end


-----------INEStudent.m-----------

#import "INEStudent.h"

@implementation INEStudent

- (void)eat {
    
    NSLog(@"INEStudent eat");
}

+ (void)drink {
    
    NSLog(@"INEStudent drink");
}

@end

控制台打印:

INEStudent eat
INEStudent drink
  • 完整消息转发

完整消息转发是指,系统会调用该类的methodSignatureForSelector:方法和forwardInvocation:方法,我们可以在这两个方法里把消息转发给别的对象,当然完整消息转发比起直接消息转发可以做更多复杂的操作,甚至你不做消息转发,自己想干什么就在forwardInvocation:方法里干什么都可以。

还是上面的例子,换成完整消息转发来实现就是:

-----------INEPerson.m-----------

#import "INEPerson.h"
#import "INEStudent.h"

@implementation INEPerson

/**
* 提供一个方法签名,用来生成invocation
*
* @param aSelector 要把哪个方法转发给别的对象——即没找到的方法
*
* @return 方法签名——即方法类型编码的包装,包含了方法的返回值和参数信息
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    if (aSelector == @selector(eat)) {
        
        // "v16@0:8":eat方法的类型编码
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

/**
 * 把消息转发给别的对象
 *
 * @param anInvocation 根据上面的方法签名生成的invocation,它是一个对象,里面封装了方法调用者、消息、方法返回值参数等信息
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation {

    // 转发给Student对象
    [anInvocation invokeWithTarget:[[INEStudent alloc] init]];
}


+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    if (aSelector == @selector(drink)) {
        
        // "v16@0:8":drink方法的类型编码
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {

    // 转发给Student对象
    [anInvocation invokeWithTarget:[INEStudent class]];
}

@end
  • 这样咋一看,完整消息转发反而比直接消息转发更麻烦了,那它有什么好处呢?

举个例子,Person对象转发一个方法后,想要获取到方法的返回值。

-----------main.m-----------

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        INEPerson *person = [[INEPerson alloc] init];
        [person addA:1 andB:2];
    }
    return 0;
}
-----------INEPerson.m-----------

@implementation INEPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(addA:andB:)) {
        
        // 通过直接消息转发,只能转发,没法获取返回值啊
        return [[INEStudent alloc] init];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

@end
-----------INEPerson.m-----------

@implementation INEPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    if (aSelector == @selector(addA:andB:)) {

        // "i24@0:8i16i20":addA:andB:方法的类型编码
        return [NSMethodSignature signatureWithObjCTypes:"i24@0:8i16i20"];
    }

    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    // 转发给Student对象
    [anInvocation invokeWithTarget:[[INEStudent alloc] init]];
    
    // 获取返回值
    int returnValue;
    [anInvocation getReturnValue:&returnValue];
    NSLog(@"%d", returnValue); // 3
}

@end

可见完整消息转发确实可以获取方法的返回值,而直接消息转发就做不到,当然这只是一个很简单的例子,完整消息转发还可以做更多复杂的操作。


四、程序崩掉unrecognized selector sent to instance


如果消息转发机制都走完了,还是没法处理这个方法的调用,那就彻底没救了,程序才会崩掉,报unrecognized selector sent to instance的错误,也就是说这个错误是消息转发机制报的,而不是消息发送机制或动态方法解析阶段报的。所以如果别人问你“什么时候会报unrecognized selector sent to instance的错误”,你最好把整个消息发送流程、动态方法解析、消息转发流程都给他说一遍,而不仅仅是说“找不到方法的实现时”——这只是消息发送阶段。


五、super关键字objc_msgSendSuper


我们知道self代表的是当前对象,可super代表的可不是父类的一个对象啊。super关键字仅仅是一个编译器指示符,它的作用就是告诉当前消息接收者直接去它的父类里的查找方法,而不是从它的类里开始查找,消息接收者还是self

super调用方法[super methodName],都会转换为objc_msgSendSuper({self, [self class]}, @selector(methodName))函数的调用,可见本质上确实还是给self发送消息,只不过直接去[self class]里查找方法而已。

下面是Runtime的源码(NSObject.mm文件):

// 返回该对象所属的类
- (Class)class {
    return object_getClass(self); 
}

// 返回该对象所属类的父类
- (Class)superclass {
    return [self class]->superclass; 
}

举个例子:

@implementation INEStudent : INEPerson

- (instancetype)init {
    
    self = [super init];
    if (self) {
        
        NSLog(@"%@", [self class]); // INEStudent
        NSLog(@"%@", [self superclass]); // INEPerson
        
        NSLog(@"%@", [super class]); // INEStudent
        NSLog(@"%@", [super superclass]); // INEPerson
    }
    
    return self;
}

@end

[self class][self superclass]就不用说了,消息接收者都是self,会从Student类的方法缓存和方法列表里开始查找classsuperclass方法,而这两个方法都是NSObject类的方法,所以会一层一层往上,找到后发现两者的实现就是返回当前消息接收者self的类和父类。

[super class][super superclass]的消息接收者其实都还是self,只不过会跳过Student类,直接从Person类的方法缓存和方法列表里开始查找classsuperclass方法,最后也还是找到NSObject类那里,找到后发现两者的实现就是返回当前消息接收者self的类和父类,而self又没变。

你可能感兴趣的:(【OC Runtime】消息发送机制,动态方法解析,消息转发机制)