美文网首页
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消息、消息转发深入源码

    1. 初探 1.1 消息 objc_msgSend 当一条消息被发送到一个实例对象时: 通过对象的isa指针找到类...

  • runtime底层实现原理

    一、Runtime介绍二、Runtime源码初探三、Runtime消息传递四、Runtime消息转发五、Runti...

  • runtime系列文章总结

    《iOS Runtime详解(消息机制,类元对象,缓存机制,消息转发)》《消息转发机制与Aspects源码解析》《...

  • iOS面试点文章链接

    runtime基础方法、用法、消息转发、super: runtime 完整总结 runloop源码、runloop...

  • Effective Objective-C读后笔记(2)

    11、runtime消息转发机制 runtime的消息转发流程图消息转发 消息转发的示例实现 这里也给大家推荐一篇...

  • 消息转发机制原理?

    iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制https://www.jians...

  • iOS - Runtime - 概念和方法交换

    runtime的概述runtime的相关概念runtime消息机制消息传递动态方法解析消息转发runtime的作用...

  • runtime 消息转发

    实例方法的消息传递:消息的转发建立在objc_msgSend(id, SEL, ...)来实现的。首先会在类对象的...

  • Runtime 消息转发

    目录 消息转发背景知识 消息转发使用方式 消息转发常见问题 消息转发背景知识 1.消息转发的定义Objective...

  • Runtime消息转发

    我们还是先从实际代码入手吧,首先,我们先新建一个类,就Person类吧,大家刚学OC的时候用的最多的就是Perso...

网友评论

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

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