美文网首页
Runtime消息、消息转发深入源码

Runtime消息、消息转发深入源码

作者: 收纳箱 | 来源:发表于2020-03-26 22:48 被阅读0次

    1. 初探

    1.1 消息

    objc_msgSend

    当一条消息被发送到一个实例对象时:

    1. 通过对象的isa指针找到类结构体,在该类结构中查找分派表中的方法选择器。
    2. 如果找不到选择器,objc_msgSend将找到父类的类结构体,在父类结构中查找分派表中的方法选择器。
    3. 如果一直找不到,继续查找父类直到NSObject类。
    4. 一旦找到选择器,函数就会调用表中的方法,并将接收对象的数据结构传递给它。

    为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址

    每个类都有一个单独的缓存,它可以包含继承方法和类中定义的方法的选择器。在搜索分派表之前,消息传递例程首先检查接收对象类的缓存。如果方法选择器在缓存中,则消息传递仅比函数调用稍慢。一旦程序运行了足够长的时间来“预热”其缓存,它发送的几乎所有消息都会找到一个缓存方法。当程序运行时,缓存会动态增长以容纳新消息。

    获取方法的地址

    避免动态绑定的唯一方法是获取方法的地址,并像调用函数一样直接调用它。当一个特定的方法将被连续执行多次,并且你希望避免每次执行该方法时消息传递的开销时,这在极少数情况下可能是合适的。

    对于在NSObjectmethodForSelector:中定义的方法,可以请求指向实现方法的过程的指针,然后使用该指针调用该过程。methodForSelector:返回的指针必须仔细转换为正确的函数类型。返回类型和参数类型都应包含在转换中。

    下面的示例显示如何调用实现setFilled:方法的过程:

    void (*setter)(id, SEL, BOOL);
    int i;
     
    setter = (void (*)(id, SEL, BOOL))[target
        methodForSelector:@selector(setFilled:)];
    for ( i = 0 ; i < 1000 ; i++ )
        setter(targetList[i], @selector(setFilled:), YES);
    

    传递给过程的前两个参数是接收对象(self)和方法选择器(_cmd)。这些参数隐藏在方法语法中,但在将方法作为函数调用时必须显式。

    使用methodForSelector:绕过动态绑定可以节省消息传递所需的大部分时间。但是,只有在特定消息重复多次的情况下(如上面所示的for循环中所示),节省的开销才是显著的。

    注意methodForSelector:由Cocoa运行时系统提供;它不是Objective-C语言本身的特性。

    1.2 动态方法解析

    比如我们声明了一个方法但没有实现它:

    @interface TestPerson : NSObject
    - (void)resolveThisMethodDynamically;
    @end
    
    @implementation TestPerson
    
    @end
    

    我们可以在resolveInstanceMethod:中通过class_addMethod进行添加:

    void dynamicMethodIMP(id self, SEL _cmd) {
        NSLog(@"%s", __func__);
    }
    
    @implementation TestPerson
    
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        NSLog(@"%s", __func__);
        if (aSEL == @selector(resolveThisMethodDynamically)) {
              class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v@:");
              return YES;
        }
        return [super resolveInstanceMethod:aSEL];
    }
    
    @end
    

    如果在resolveInstanceMethod:中设置了断点:

    resolveInstanceMethod

    我们可以看到首先方法没有被缓存,调用_objc_msgSend_uncached(),之后查找IMP或者转发lookUpImpOrForward(),然后来到resolveInstanceMethod:

    这是发生在消息转发之前的,在执行完class_addMethod并返回YES后就把选择器和对应的实现添加到类里面了,同时会进行缓存。

    2020-03-25 13:31:38.152558+0800 RuntimeDemo[61884:2844585] +[TestPerson resolveInstanceMethod:]
    2020-03-25 13:31:54.485904+0800 RuntimeDemo[61884:2844585] dynamicMethodIMP
    2020-03-25 13:31:54.486149+0800 RuntimeDemo[61884:2844585] dynamicMethodIMP
    

    可以看到,resolveInstanceMethod:只调用了一次,就是因为这个选择器和实现已经被缓存起来了。

    1.3 消息转发

    当一条消息被发送到一个实例对象时:

    1. 通过对象的isa指针找到类结构体,在该类结构中查找分派表中的方法选择器。
    2. 如果找不到选择器,objc_msgSend将找到父类的类结构体,在父类结构中查找分派表中的方法选择器。
    3. 如果一直找不到,继续查找父类直到NSObject类。
    4. 一旦找到选择器,函数就会调用表中的方法,并将接收对象的数据结构传递给它。

    当一条消息被发送到一个实例对象时,和上面类似,但不同的是类方法是存储在元类中的

    isa

    如果一个实例方法不能在类和继承链的方法列表中不能被找到,则会进入方法解析和消息转发流程:

    1. 首先判断当前实例的类对象是否实现了resolveInstanceMethod:方法,如果实现的话,会调用 resolveInstanceMethod方法。这个时候我们可以在resolveInstanceMethod方法里动态的添加该SEL对应的方法。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现resolveInstanceMethod:方法,则会进入消息转发。
    2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下一个尝试。
    3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
    4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。如果调用[super forwardInvocation:]则直接调用doesNotRecognizeSelector抛出异常。
    消息转发

    2. 底层探究

    2.1 消息

    实例方法

    Objective-C的对象是基于Runtime创建的结构体。

    @interface Father : NSObject
    @end
    @implementation Father
    @end
      
    int main(int argc, const char * argv[])
    {
      @autoreleasepool {
          Father *father = [[Father alloc] init];
      }
      return 0;
    }
    

    alloc方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化isa_t,而isa_t是一个union类型的结构体(或者称之为联合体),它的结构是在Runtime里被定义的。

    union isa_t {  
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
        struct {
           uintptr_t indexed           : 1;
           uintptr_t has_assoc         : 1;
           uintptr_t has_cxx_dtor      : 1;
           uintptr_t shiftcls          : 33;
           uintptr_t magic             : 6;
           uintptr_t weakly_referenced : 1;
           uintptr_t deallocating      : 1;
           uintptr_t has_sidetable_rc  : 1;
           uintptr_t extra_rc          : 19;
        };
    };
    

    isa_t的结构可以看出,isa_t可以存储structuintptr_t或者Class类型。

    init方法就直接返回了初始化好的对象,father指针指向这个初始化好的对象。

    类方法

    这个对象只存放了一个isa_t结构体和成员变量,对象的方法在哪里?

    struct objc_class : objc_object {
        isa_t isa;
        Class superclass;
        cache_t cache;            
        class_data_bits_t bits;    
    }
    

    我们看到,类对象里同样储存着一个isa_t的结构体,super_class指针,cache_t结构体,class_data_bits_t指针。

    其中class_data_bits_t指向类对象的数据区域,数据区域存放着这个类的实例方法链表

    类方法存在元类对象的数据区域。也就是说,有对象,类对象,元类对象三个概念:

    • 对象是在运行时动态创建的,可以有无数个。
    • 类对象和元类对象在main方法之前创建的,分别只会有一个。
    objc_msgSend

    在源码中objc_msgSend是以汇编的形式存在的,我们来看一下arm64的:

    /********************************************************************
     *
     * id objc_msgSend(id self, SEL _cmd, ...);
     * IMP objc_msgLookup(id self, SEL _cmd, ...);
     * 
     * objc_msgLookup ABI:
     * IMP returned in x17
     * x16 reserved for our use but not used
     *
     ********************************************************************/
    
    #if SUPPORT_TAGGED_POINTERS
        .data
        .align 3
        .globl _objc_debug_taggedpointer_classes
    _objc_debug_taggedpointer_classes:
        .fill 16, 8, 0
        .globl _objc_debug_taggedpointer_ext_classes
    _objc_debug_taggedpointer_ext_classes:
        .fill 256, 8, 0
    #endif
    
        ENTRY _objc_msgSend
        UNWIND _objc_msgSend, NoFrame
    
    // 1.是否为空,直接返回
        cmp p0, #0          // nil check and tagged pointer check
    #if SUPPORT_TAGGED_POINTERS
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
        b.eq    LReturnZero
    #endif
        ldr p13, [x0]       // p13 = isa
        GetClassFromIsa_p16 p13     // p16 = class
    // 2.isa处理完毕
    LGetIsaDone:
    // 3. 查找缓存
        // calls imp or objc_msgSend_uncached
        CacheLookup NORMAL, _objc_msgSend
    
    #if SUPPORT_TAGGED_POINTERS
    LNilOrTagged:
        b.eq    LReturnZero     // nil check
    
        // tagged
        adrp    x10, _objc_debug_taggedpointer_classes@PAGE
        add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
        ubfx    x11, x0, #60, #4
        ldr x16, [x10, x11, LSL #3]
        adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
        add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
        cmp x10, x16
        b.ne    LGetIsaDone
    
        // ext tagged
        adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
        add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
        ubfx    x11, x0, #52, #8
        ldr x16, [x10, x11, LSL #3]
        b   LGetIsaDone
    // SUPPORT_TAGGED_POINTERS
    #endif
    
    LReturnZero:
        // x0 is already zero
        mov x1, #0
        movi    d0, #0
        movi    d1, #0
        movi    d2, #0
        movi    d3, #0
        ret
    
        END_ENTRY _objc_msgSend
    

    可以看到整个过程是这样的:

    1. LReturnZero:判断了是否为空,直接返回
    2. LGetIsaDone:isa处理完毕
    3. CacheLookup NORMAL:查找缓存

    下面看CacheLookup NORMAL部分:

    .macro CacheLookup
        //
        // Restart protocol:
        //
        //   As soon as we're past the LLookupStart$1 label we may have loaded
        //   an invalid cache pointer or mask.
        //
        //   When task_restartable_ranges_synchronize() is called,
        //   (or when a signal hits us) before we're past LLookupEnd$1,
        //   then our PC will be reset to LLookupRecover$1 which forcefully
        //   jumps to the cache-miss codepath which have the following
        //   requirements:
        //
        //   GETIMP:
        //     The cache-miss is just returning NULL (setting x0 to 0)
        //
        //   NORMAL and LOOKUP:
        //   - x0 contains the receiver
        //   - x1 contains the selector
        //   - x16 contains the isa
        //   - other registers are set as per calling conventions
        //
    LLookupStart$1:
    
        // p1 = SEL, p16 = isa
        ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets
        and p12, p1, p11, LSR #48       // x12 = _cmd & mask
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        and p10, p11, #~0xf         // p10 = buckets
        and p11, p11, #0xf          // p11 = maskShift
        mov p12, #0xffff
        lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
        and p12, p1, p11                // x12 = _cmd & mask
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    
    
        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
      // 1.缓存命中
        CacheHit $0         // call or return imp
        
    2:  // not hit: p12 = not-hit bucket
        // 2.没有找到方法
        CheckMiss $0            // miss if bucket->sel == 0
        cmp p12, p10        // wrap if bucket == buckets
        b.eq    3f
        ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
        b   1b          // loop
    
    3:  // wrap: p12 = first bucket, w11 = mask
    // 继续查找
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                        // p12 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        add p12, p12, p11, LSL #(1+PTRSHIFT)
                        // p12 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    
        // Clone scanning loop to miss instead of hang when cache is corrupt.
        // The slow path may detect any corruption and halt later.
    
        ldp p17, p9, [x12]      // {imp, sel} = *bucket
    1:  cmp p9, p1          // if (bucket->sel != _cmd)
        b.ne    2f          //     scan more
        CacheHit $0         // call or return imp
        
    2:  // not hit: p12 = not-hit bucket
        CheckMiss $0            // miss if bucket->sel == 0
        cmp p12, p10        // wrap if bucket == buckets
        b.eq    3f
        ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
        b   1b          // loop
    
    LLookupEnd$1:
    LLookupRecover$1:
    3:  // double wrap
        JumpMiss $0
    
    .endmacro
    

    主要有3个方法:

    1. CacheHit:缓存命中,直接调用或返回IMP
    2. CheckMiss:没有找到方法
    3. add:继续查找

    下面我们看CheckMiss

    .macro CheckMiss
        // miss if bucket->sel == 0
    .if $0 == GETIMP
        cbz p9, LGetImpMiss
    .elseif $0 == NORMAL
        cbz p9, __objc_msgSend_uncached
    .elseif $0 == LOOKUP
        cbz p9, __objc_msgLookup_uncached
    .else
    .abort oops
    .endif
    .endmacro
    

    还记得objc_msgSend我们调用的是CacheLookup NORMAL,那么我们会跳转到__objc_msgSend_uncached

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgSend_uncached
    

    下面我们来到MethodTableLookup

    .macro MethodTableLookup
        
        // push frame
        SignLR
        stp fp, lr, [sp, #-16]!
        mov fp, sp
    
        // save parameter registers: x0..x8, q0..q7
        sub sp, sp, #(10*8 + 8*16)
        stp q0, q1, [sp, #(0*16)]
        stp q2, q3, [sp, #(2*16)]
        stp q4, q5, [sp, #(4*16)]
        stp q6, q7, [sp, #(6*16)]
        stp x0, x1, [sp, #(8*16+0*8)]
        stp x2, x3, [sp, #(8*16+2*8)]
        stp x4, x5, [sp, #(8*16+4*8)]
        stp x6, x7, [sp, #(8*16+6*8)]
        str x8,     [sp, #(8*16+8*8)]
    
        // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
        // receiver and selector already in x0 and x1
        mov x2, x16
        mov x3, #3
        bl  _lookUpImpOrForward
    
        // IMP in x0
        mov x17, x0
        
        // restore registers and return
        ldp q0, q1, [sp, #(0*16)]
        ldp q2, q3, [sp, #(2*16)]
        ldp q4, q5, [sp, #(4*16)]
        ldp q6, q7, [sp, #(6*16)]
        ldp x0, x1, [sp, #(8*16+0*8)]
        ldp x2, x3, [sp, #(8*16+2*8)]
        ldp x4, x5, [sp, #(8*16+4*8)]
        ldp x6, x7, [sp, #(8*16+6*8)]
        ldr x8,     [sp, #(8*16+8*8)]
    
        mov sp, fp
        ldp fp, lr, [sp], #16
        AuthenticateLR
    
    .endmacro
    

    里面最重要的就是_lookUpImpOrForward,这时我们已经在汇编中搜不到了,但是其实这个C函数,我们在工程里面搜索lookUpImpOrForward就能找到。

    IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                           bool initialize, bool cache, bool resolver)
    {
        IMP imp = nil;
        bool triedResolver = NO;
    
        runtimeLock.assertUnlocked();
    
        // 如果cache是YES,则从缓存中查找IMP。
        if (cache) {
            // 通过cache_getImp函数查找IMP,查找到则返回IMP并结束调用
            imp = cache_getImp(cls, sel);
            if (imp) return imp;
        }
    
        runtimeLock.read();
    
        // 判断类是否已经被创建,如果没有被创建,则将类实例化
        if (!cls->isRealized()) {
            runtimeLock.unlockRead();
            runtimeLock.write();
            
            // 对类进行实例化操作
            realizeClass(cls);
    
            runtimeLock.unlockWrite();
            runtimeLock.read();
        }
    
        // 第一次调用当前类的话,执行initialize的代码
        if (initialize  &&  !cls->isInitialized()) {
            runtimeLock.unlockRead();
            // 对类进行初始化,并开辟内存空间
            _class_initialize (_class_getNonMetaClass(cls, inst));
            runtimeLock.read();
        }
        
     retry:    
        runtimeLock.assertReading();
    
        // 尝试获取这个类的缓存
        imp = cache_getImp(cls, sel);
        if (imp) goto done;
    
        {
            // 如果没有从cache中查找到,则从方法列表中获取Method
            Method meth = getMethodNoSuper_nolock(cls, sel);
            if (meth) {
                // 如果获取到对应的Method,则加入缓存并从Method获取IMP
                log_and_fill_cache(cls, meth->imp, sel, inst, cls);
                imp = meth->imp;
                goto done;
            }
        }
    
        // Try superclass caches and method lists.
        {
            unsigned attempts = unreasonableClassCount();
            // 循环获取这个类的缓存IMP 或 方法列表的IMP
            for (Class curClass = cls->superclass;
                 curClass != nil;
                 curClass = curClass->superclass)
            {
                // Halt if there is a cycle in the superclass chain.
                if (--attempts == 0) {
                    _objc_fatal("Memory corruption in class list.");
                }
                
                // Superclass cache.
                // 获取父类缓存的imp
                imp = cache_getImp(curClass, sel);
                if (imp) {
                    if (imp != (IMP)_objc_msgForward_impcache) {
                        // Found the method in a superclass. Cache it in this class.
                        // 如果发现父类的方法,并且不在缓存中,在下面的函数中缓存方法
                        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.
                // 在父类的方法列表中,获取method_t对象。如果找到则缓存查找到的IMP
                Method meth = getMethodNoSuper_nolock(curClass, sel);
                if (meth) {
                    log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                    imp = meth->imp;
                    goto done;
                }
            }
        }
    
        // 如果没有找到,则尝试动态方法解析
        if (resolver  &&  !triedResolver) {
            runtimeLock.unlockRead();
            _class_resolveMethod(cls, sel, inst);
            runtimeLock.read();
            // 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;
        }
    
        // No implementation found, and method resolver didn't help. 
        // Use forwarding.
    
        // 如果没有IMP被发现,并且动态方法解析也没有处理,则进入消息转发阶段
        imp = (IMP)_objc_msgForward_impcache;
        cache_fill(cls, sel, imp, inst);
    
     done:
        runtimeLock.unlockRead();
    
        return imp;
    }
    

    lookUpImpOrForward比较长,我们看核心的部分:

    1. 如果cache是YES,则从缓存中查找IMP。也就是说如果之前执行过的方法,就在缓存中有,就不需要下面的操作了。
    2. 判断类是否已经被创建,如果没有被创建,则将类实例化。
    3. 第一次调用当前类的话,执行initialize的代码。
    4. 尝试获取这个类的缓存。
    5. 如果没有从cache中查找到,则从自己的方法列表中获取method
    6. 如果还没有,就从父类缓存或者方法列表获取IMP
    7. 如果没有找到,则尝试动态方法解析。
    8. 如果没有IMP被找到,并且动态方法解析也没有处理,则进入消息转发阶段。

    这里再扩展一下,类对象里有一个cache_t结构体用于方法命中后的缓存,它的结构如下:

    struct cache_t {
        struct bucket_t *_buckets; //一个散列表,用来方法缓存
        mask_t _mask; // 分配用来缓存bucket的总数
        mask_t _occupied; // 表明目前实际占用的缓存bucket的个数
    }
      
    struct bucket_t {
    private:
        cache_key_t _key; // 缓存key
        IMP _imp; // 方法实现imp
    }
    

    如果没有命中,则会从类对象的class_data_bits_t指针找到数据区域,数据区域里用链表存放着类的实例方法。实例方法也是一个结构体,其结构为:

    struct method_t {  
        SEL name; // 方法选择器
        const char *types; // 编译器将每个方法的返回值和参数类型编码为一个字符串
        IMP imp; // 方法实现imp
    };
    

    objc_msgSend会在类对象的方法链表里按链表顺序去匹配SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets缓存起来。如果没找到则会通过类对象的superclass指针找到其父类,去父类的方法列表里寻找,直到NSObject

    注意:父类中查找也是从缓存开始的,然后才是方法链表。

    2.2 动态方法解析

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

    这里就到了我们熟悉的动态方法解析。

    • 如果我们调用的是实例方法,那么cls就不是元类,就会执行实例方法的动态解析。
    • 如果我们调用的是类方法,cls就是元类,先调用类方法的动态解析。如果没有找到,我们还会调用实例方法的动态解析。这里调用元类的实例方法,会从根元类(元类isa指向根元类)开始找,最终会找到NSObjectresolveInstanceMethod实例方法。

    注意:类方法,存储在元类中是实例方法。

    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));
            }
        }
    }
    
    static inline IMP
    lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
    {
        return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
    }
    

    resolveInstanceMethodresolveClassMethod都会看cls是否实现了对应的方法,然后给这个类发消息,如果动态解析提供了方法,那么下次lookUpImpOrNil就会命中。resolveMethod_locked最后就会返回对应的IMP

    2.3 消息转发

    // 如果没有IMP被发现,并且动态方法解析也没有处理,则进入消息转发阶段
    imp = (IMP)_objc_msgForward_impcache;
    

    这部分又回到了汇编代码__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
    

    下面我们又回到了C函数_objc_forward_handler

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    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;
    

    这就是我们最常看到的unrecognized selector sent to instance了。但是,等一下,说好的消息转发呢?

    这部分苹果没有开源,所以没法看。但是我们怎么知道中间有这些过程的呢...其实我们还有一个神器:

    @interface Father : NSObject
    - (void)test;
    @end
    @implementation Father
    @end
    
    // 内部的一个打印信息的函数
    extern void instrumentObjcMessageSends(BOOL flag);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            instrumentObjcMessageSends(YES);
            [[[Father alloc] init] test];
            instrumentObjcMessageSends(NO);
        }
        return 0;
    }
    

    这个方法开启之后会在/private/tmp目录下创建一个msgSends-xxxxxx是内部生成的一个编号。

    msgSends-1427

    看到调用里面,对对象进行了初始化,然后调用了

    1. resolveInstanceMethod
    2. forwardingTargetForSelector
    3. methodSignatureForSelector
    4. resolveInstanceMethod
    5. doesNotRecognizeSelector

    你可能要问,那forwardInvocation呢?这是因为我们有处理methodSignatureForSelector,这时是不会调用forwardInvocation的。现在我们加上:

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSLog(@"%s", __FUNCTION__);
        if (aSelector == @selector(test)) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    

    现在再运行一次,就有了:

    forwardInvocation
    如果你有Hopper Disassembler

    我们先看看崩溃时的调用堆栈:

    2020-03-28 16:11:36.785540+0800 KCObjcTest[8089:212794] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Father test]: unrecognized selector sent to instance 0x100530f90'
    *** First throw call stack:
    (
        0   CoreFoundation     0x00007fff3c8218ab __exceptionPreprocess + 250
        1   libobjc.A.dylib    0x00000001002cf1e0 objc_exception_throw + 48
        2   CoreFoundation     0x00007fff3c8a0b61 -[NSObject(NSObject) __retain_OA] + 0
        3   CoreFoundation     0x00007fff3c785adf ___forwarding___ + 1427
        4   CoreFoundation     0x00007fff3c7854b8 _CF_forwarding_prep_0 + 120
        5   KCObjcTest         0x0000000100000f40 main + 64
        6   libdyld.dylib      0x00007fff73e497fd start + 1
        7   ???                0x0000000000000001 0x0 + 1
    )
    libc++abi.dylib: terminating with uncaught exception of type NSException
    

    现在我们的首要目标就是找到_CF_forwarding_prep_0
    首先,把这个Mach-O拷出来,并拖入Hopper Disassembler

    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
    

    找到___forwarding_prep_0___

    ___forwarding_prep_0___

    下一步是____forwarding___

    ____forwarding___

    ____forwarding___发现了forwardingTargetForSelector

    forwardingTargetForSelector:

    阅读之后发现,没有实现或者返回为空则会跳转到loc_140277

    methodSignatureForSelector:

    loc_140277下面几行发现了methodSignatureForSelector,如果没有实现、或返回为空会跳转。我们这里继续看,如果没有实现_forwardStackInvocation:,则会跳转loc_14041b

    forwardInvocation:

    在这里我们看到,如果实现了forwardInvocation:,则会调用forwardInvocation:

    这样也可以看到整个流程。

    3. Runtim简单的例子

    我们做一个简单的例子,在ViewController动态创建另一个VC,然后push出来。

    - (void)pushToAnyVCWithData:(NSDictionary *)dataDict
    {
        // 实例化对象
        id instance = nil;
        //从字典获取类名
        const char *clsName = [dataDict[@"class"] UTF8String];
        // 获取类对象
        Class cls = objc_getClass(clsName);
        // 尝试从Storyboard初始化
        @try {
            UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
            instance = [sb instantiateViewControllerWithIdentifier:dataDict[@"class"]];
        } @catch (NSException *exception) {
            // 如果Storyboard中没有,则动态创建类
            if (!cls) {
                // 获取父类对象
                Class superClass = [UIViewController class];
                cls = objc_allocateClassPair(superClass, clsName, 0);
                class_addIvar(cls, "ending", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
                class_addIvar(cls, "show_lb", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
                objc_registerClassPair(cls);
                //⚠️注意: 考点1,添加ivar需要在objc_registerClassPair之前,下面这样是不行的
                //class_addIvar(cls, "ending", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
                //class_addIvar(cls, "show_lb", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
                
                // 把方法添加到动态类对象中
                // ⚠️注意: 考点2,这个可以在objc_registerClassPair之后
                Method method = class_getInstanceMethod([self class], @selector(lg_instancemethod));
                IMP methodIMP = method_getImplementation(method);
                const char *types = method_getTypeEncoding(method);
                BOOL rest = class_addMethod(cls, @selector(viewDidLoad), methodIMP, types);
                NSLog(@"rest == %d",rest);
            }
            instance = [[cls alloc] init];
        } @finally {
            NSLog(@"OK");
        }
        
        NSDictionary *dict = dataDict[@"data"];
        [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            // 检测是否存在key的属性
            if (class_getProperty(cls, [key UTF8String])) {
                [instance setValue:obj forKey:key];
            }
            // 检测是否存在key的变量
            else if (class_getInstanceVariable(cls, [key UTF8String])){
                [instance setValue:obj forKey:key];
            }
        }];
        
        [self.navigationController pushViewController:instance animated:YES];
    }
    

    我们再来看看动态添加的方法:

    - (void)lg_instancemethod
    {
        // ⚠️注意:考点,self是谁?
        // 答案:objc_msgSend,谁调用是谁。self只是型参。
        [super viewDidLoad];
        // 动态添加的方法,只能通过KVC实现赋值
        [self setValue:[UIColor orangeColor] forKeyPath:@"view.backgroundColor"];
        [self setValue:[[UILabel alloc] initWithFrame:CGRectMake(100, 200, 200, 30)] forKey:@"show_lb"];
        UILabel *show_lb = [self valueForKey:@"show_lb"];
        [self.view addSubview:show_lb];
        show_lb.text = [self valueForKey:@"ending"];
        show_lb.font = [UIFont systemFontOfSize:14];
        show_lb.textColor = [UIColor blackColor];
        show_lb.textAlignment = NSTextAlignmentCenter;
        show_lb.backgroundColor = [UIColor whiteColor];
        NSLog(@"hello word");
    }
    

    相关文章

      网友评论

          本文标题:Runtime消息、消息转发深入源码

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