iOS - objc_msgSend()详解

[toc]

参考

objc_msgSend() 详解

objc4

//www.greatytc.com/p/1bde36ad9938

objc_msgSend() 简介

方法调用本质

OC中的方法调用, 其实都是转换为 objc_msgSend() 函数的调用;

消息机制: 给方法调用者发送消息

Person *person = [[Person alloc] init];
// objc_msgSend(person, @selector(personTest));
// 消息接收者(receiver):person
// 消息名称:personTest
[person personTest];

// objc_msgSend([Person class], @selector(initialize));
// 消息接收者(receiver):[Person class]
// 消息名称:initialize
[Person initialize];
流程

objc_msgSend 的执行流程可以分为3大阶段

  • 消息发送

  • 动态方法解析

  • 消息转发

补救方案

首先, 调用方法时, 系统会查看这个对象能否接收这个消息 (查看这个类有没有这个方法, 或者有没有实现这个方法。)

如果不能, 就会调用下面这几个方法, 给你“补救”的机会, 你可以先理解为几套防止程序crash的备选方案, 我们就是利用这几个方案进行消息转发;

注意, 前一套方案实现, 后一套方法就不会执行。

如果这几套方案你都没有做处理, 那么 objc_msgSend() 最后找不到合适的方法进行调用, 会报错 unrecognized selector sent to instance

方案1 (动态方法解析, 添加方法):

// 首先,系统会调用resolveInstanceMethod或resolveClassMethod 让你自己为这个方法动态增加实现。动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

方案2 (消息转发):

// 如果不对resolveInstanceMethod做任何处理, 系统会来到方案二, 走forwardingTargetForSelector方法,我们可以返回其他实例对象, 实现消息转发。
- (id)forwardingTargetForSelector:(SEL)aSelector;

方案3 (消息转发调用):

// 如果不实现 forwardingTargetForSelector, 系统就会调用方案三的两个方法 方法签名&转发调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
流程图示
image

消息发送 😳

查找方法的执行流程

图示

image

源码解读

objc_msgSend()

因为这个方法调用频次太高, 所以使用汇编实现提高效率

// objc-msg-arm64.s

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    // p0寄存器, 存放的是消息接收者 receiver, 判断是否为0
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS 
    // b是跳转; 如果 le (p0≤0)成立, 则跳转到 LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    // 如果消息接收者不为空
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // 查找方法缓存, 见下面分析 ★
    // 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
CacheLookup

查找方法缓存

// 宏定义
.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
    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

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
CheckMiss

缓存没有命中

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

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

__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
__objc_msgLookup_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search

MethodTableLookup
ret

END_ENTRY __objc_msgLookup_uncached
_cache_getImp
    STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp
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
    // 汇编里面 b 开头的指令一般是跳转调用
    // c函数在编译成汇编之后, 会多一条下划线, 找到c的 lookUpImpOrForward, 返回的是函数地址 IMP ★
    // 传入的参数 behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER  = 0b0001 | 0b0010 = 0b0011
    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()

MethodTableLookup 和 resolveMethod_locked() 调用

// objc-runtime-new.mm
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    // 消息转发
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup  乐观(尝试)查找一下缓存, 因为这期间有可能别的地方有调用了这个方法, 添加到了缓存
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
    
    runtimeLock.assertLocked();
    // 遍历用的`游标`(类/元类)
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().
    // 如果当前类的缓存中没有, 则遍历, 根据superclass指针向上一层层查找 ★★
    for (unsigned attempts = unreasonableClassCount();;) {
        
        // 根据方法名 sel 去当前`游标`中查找方法 ★ 
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel); // ★
        if (meth) {
            // 取出函数地址 IMP
            imp = meth->imp;
            goto done;
        }
        // 游标向父类步进, 如果直到根类都没找到, 进入消息转发 ★
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }
        
        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
        
        // 如果自身没找到, 去父类的缓存和方法列表中查找 ★
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // 从父类中找到了方法, 填充到当前类的缓存 ★★
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
    
    // No implementation found. Try method resolver once.
    // 首次查找时, 没找到, 尝试一次动态方法解析 ★★★
    // 此时, behavior = 0b0011; &= LOOKUP_RESOLVER 为 0b0010, 可以进入if(); 
    
    // 由于 resolveMethod_locked 内部调用了本函数, 而如果 resolveMethod_locked 并没有动态添加方法, 会再次来到这里
    // 此时, behavior = 0b0101; &= LOOKUP_RESOLVER 为 0b0000, 不能进入if();
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // behavior = 0b0011 ^ 0b0010 = 0b0001
        behavior ^= LOOKUP_RESOLVER;
        // 该函数内部会回调本函数, behavior = behavior | LOOKUP_CACHE = 0b0101  ★
        return resolveMethod_locked(inst, sel, cls, behavior); 
    }

 done:
    // 填充缓存 ★
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock: // 
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}


enum {
    LOOKUP_INITIALIZE = 1, // 0b0001
    LOOKUP_RESOLVER = 2, // 0b0010
    LOOKUP_CACHE = 4, // 0b0100
    LOOKUP_NIL = 8, // 0b1000
};
getMethodNoSuper_nolock()

根据方法名 sel 去 类/元类 中查找方法

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) {
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    
    // cls->data() 即 bits & FAST_DATA_MASK 可以获取到 struct class_rw_t, 然后从中拿到 methods (二维数组) ★
    auto const methods = cls->data()->methods();
    // 遍历二维数组, 找到 method_t
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel); // ★
        if (m) return m;
    }

    return nil;
}
search_method_list_inline()
ALWAYS_INLINE static method_t *search_method_list_inline(const method_list_t *mlist, SEL sel) {
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        // 排序的方法列表 => 二分查找 ★★
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 乱序的方法列表 => 线性查找
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
findMethodInSortedMethodList()
ALWAYS_INLINE static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) {
    ASSERT(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // 二分查找
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
log_and_fill_cache()

填充缓存 - 在 lookUpImpOrForward 中被调用

static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill(cls, sel, imp, receiver); // ★
}
cache_fill()

填充缓存

void cache_fill(Class cls, SEL sel, IMP imp, id receiver) {
    runtimeLock.assertLocked();

#if !DEBUG_TASK_THREADS
    // Never cache before +initialize is done
    if (cls->isInitialized()) {
        cache_t *cache = getCache(cls);
#if CONFIG_USE_CACHE_LOCK
        mutex_locker_t lock(cacheUpdateLock);
#endif
        // 调用 cache_t::insert 将新调用的方法插入到当前类对象的方法缓存中 ★
        cache->insert(cls, sel, imp, receiver);
    }
#else
    _collecting_in_critical();
#endif
}

动态方法解析 😳

图示

image

源码解读

resolveMethod_locked()

在 lookUpImpOrForward() 中有调用

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);
        }
    }
    
    // 再次调用 `lookUpImpOrForward()` 入参 behavior = 0b0001 | 0b0100 = 0b0101
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
resolveInstanceMethod()
/
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
*/
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;
    }
    // 给传入的 cls 发送 `resolveInstanceMethod:` ★ 
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    
    // 这个返回值只是做了一些打印, 所以 `resolveInstanceMethod:` 的返回值为 YES / NO 实际效果都一样
    // `resolveInstanceMethod:` 不添加方法实现, 仅仅返回 YES 是没用的 ★
    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));
        }
    }
}
resolveClassMethod()
/*
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
*/
static void resolveClassMethod(id inst, SEL sel, Class cls) {
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        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);
        }
    }
    // 让元类调用 `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 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));
        }
    }
}

动态添加方法

开发者可以实现以下方法, 来动态添加方法实现

+ (BOOL)resolveInstanceMethod:(SEL)sel;

+ (BOOL)resolveClassMethod:(SEL)sel;
示例
// c函数名就是函数地址
void c_other(id self, SEL _cmd) {
    NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        // 动态添加test方法的实现, 将方法添加到 class_rw_t 的 methods 中 ★
        class_addMethod(self, sel, (IMP)c_other, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(test)) {
        // 第一个参数是object_getClass(self) 元类对象
        class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

动态解析过后, 会重新走“消息发送”的流程

<u>“从receiverClass 的 cache中查找方法”这一步开始执行</u>

消息转发 😳

自己及父类都无法处理该消息, 也没有动态方法解析, 会进入消息转发阶段, 将消息转发给其他实例 /类 (备用接收者)

注意, 消息机制支持, 类方法, 实例方法和类方法本质没有区别, 都是消息机制。

当对象不能接受某个selector时, 如果不对 resolveInstanceMethod 做任何处理, 系统会来到 forwardingTargetForSelector 方法, 我们可以返回其他实例对象, 实现消息转发。

图示

image

使用

forwardingTargetForSelector
// 对于类方法的转发
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    // 这里甚至可以返回实例对象, 转发给实例对象来调用, 相当于:
    // objc_msgSend([[Cat alloc] init], @selector(test))
    // [[[Cat alloc] init] test]
    if (aSelector == @selector(test)) return [[Cat alloc] init];

    return [super forwardingTargetForSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        // 返回一个实现了该方法的对象, 实际相当于调用了下面的方法
        // `objc_msgSend([[Animal alloc] init], aSelector)`
        return [[Animal alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 若返回的对象如果没有实现该方法, 相当于返回了nil 
  • 若实现了该方法, 且返回值不空, 则将消息转发给其他对象

  • 若未实现该方法, 或者返回了nil, 会调用 methodSignatureForSelector:, 要求返回方法签名

methodSignatureForSelector
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        // 手写方法签名
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
        // 也可以让一个实现了该方法的对象, 调用本方法, 并返回结果
        // return [[[Xxx alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
  • 若实现了该方法, 且返回值不空, 则将其返回的方法签名封装到 NSInvocation,并调用 forwardInvocation: ;

    其中, 方法签名决定了 NSInvocation 方法的返回值及参数数量、类型。

  • 若未实现该方法, 或者返回了nil, 会调用 doesNotRecognizeSelector:, 直接报找不到方法; ★

forwardInvocation
/*
* NSInvocation 封装了一个方法调用, 包括: 方法调用者、方法名、方法参数
* anInvocation.target 方法调用者, 默认是最开始接收消息的对象
* anInvocation.selector 方法名
* [anInvocation getArgument:NULL atIndex:0] 调用时传入的参数
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//    anInvocation.target = [[Cat alloc] init];
//    [anInvocation invoke];
 
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
}
  • 该方法内, 不是必须调用 invoke,

  • 实际上这个方法内部是可以做任何事, 甚至不做任何事;
    比如可以修改参数, 修改返回值等等;

NSMethodSignature 顾名思义应该就是“方法签名”, 类似于C++中的编译器时的函数签名。苹果官方定义该类为对方法的参数、返回类似进行封装, 协同NSInvocation实现消息转发。通过消息转发实现类似C++中的多重继承。

iOS中的SEL, 它的作用和C、C++中的函数指针很相似, 通过performSelector:withObject:函数可以直接调用这个消息。

但是perform相关的这些函数, 有一个局限性, 其参数数量不能超过2个, 否则要做很麻烦的处理, 与之相对, NSInvocation也是一种消息调用的方法, 并且它的参数没有限制。这两种直接调用对象消息的方法, 在IOS4.0之后, 大多被block结构所取代, 只有在很老的兼容性系统中才会使用。

源码解读

__objc_msgForward_impcache

lookUpImpOrForward() 中调用

// objc-msg-arm64.s
STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b   __objc_msgForward

END_ENTRY __objc_msgForward_impcache
__objc_msgForward
ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward
_objc_forward_handler
// objc-runtime.mm
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

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

这里没有开源, 只有写打印信息; 接下来就需要逆向分析汇编代码了;

先考虑换一种思路, 如果不实现消息转发, Xcode崩溃会输出函数调用栈:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person test]: unrecognized selector sent to instance 0x1004a2f50'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff2ee22be7 __exceptionPreprocess + 250
    1   libobjc.A.dylib                     0x00007fff67bfa5bf objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff2eea1c77 -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff2ed8744b ___forwarding___ + 1427
    4   CoreFoundation                      0x00007fff2ed86e28 _CF_forwarding_prep_0 + 120
    
    6   libdyld.dylib                       0x00007fff68da1cc9 start + 1
    7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
__forwarding__

其中, ___forwarding___ 会调用 forwardingTargetForSelector:

// 国外开发者根据汇编写出的伪代码
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 调用 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            if (isStret == 1) {
                int ret;
                objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
                return ret;
            }
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 僵尸对象
    const char *className = class_getName(receiverClass);
    const char *zombiePrefix = "_NSZombie_";
    size_t prefixLen = strlen(zombiePrefix); // 0xa
    if (strncmp(className, zombiePrefix, prefixLen) == 0) {
        CFLog(kCFLogLevelError,
              @"*** -[%s %s]: message sent to deallocated instance %p",
              className + prefixLen,
              selName,
              receiver);
        <breakpoint-interrupt>
    }
    // 如果 `forwardingTargetForSelector:` 没有实现, 或者返回了nil; 会调用 `methodSignatureForSelector:`, 要求返回方法签名
    // 调用 methodSignatureForSelector 获取方法签名后, 再调用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature) {
            BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
            if (signatureIsStret != isStret) {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
                      selName,
                      signatureIsStret ? "" : not,
                      isStret ? "" : not);
            }
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

                [receiver forwardInvocation:invocation];

                void *returnValue = NULL;
                [invocation getReturnValue:&value];
                return returnValue;
            } else {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
                      receiver,
                      className);
                return 0;
            }
        }
    }

    SEL *registeredSel = sel_getUid(selName);

    // selector 是否已经在 Runtime 注册过
    if (sel != registeredSel) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
              sel,
              selName,
              registeredSel);
    } // doesNotRecognizeSelector ★
    else if (class_respondsToSelector(receiverClass, @selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }
    else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
              receiver,
              className);
    }

    // The point of no return.
    kill(getpid(), 9);
}

消息转发的应用

  • 容灾处理: 防止找不到方法, 产生崩溃
@implementation Person
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 本来能调用的方法
    if ([self respondsToSelector:aSelector]) {
        return [super methodSignatureForSelector:aSelector];
    }
    // 找不到的方法
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 找不到的方法, 都会来到这里
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 可以在这里收集找不到的方法
    NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
}
@end
  • NSProxy 专门用来做消息转发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容