iOS - objc_msgSend()詳解

[toc]

參考

objc_msgSend() 詳解

objc4

http://www.reibang.com/p/1bde36ad9938

objc_msgSend() 簡介

方法調(diào)用本質(zhì)

OC中的方法調(diào)用, 其實(shí)都是轉(zhuǎn)換為 objc_msgSend() 函數(shù)的調(diào)用;

消息機(jī)制: 給方法調(diào)用者發(fā)送消息

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 的執(zhí)行流程可以分為3大階段

  • 消息發(fā)送

  • 動態(tài)方法解析

  • 消息轉(zhuǎn)發(fā)

補(bǔ)救方案

首先, 調(diào)用方法時, 系統(tǒng)會查看這個對象能否接收這個消息 (查看這個類有沒有這個方法, 或者有沒有實(shí)現(xiàn)這個方法。)

如果不能, 就會調(diào)用下面這幾個方法, 給你“補(bǔ)救”的機(jī)會, 你可以先理解為幾套防止程序crash的備選方案, 我們就是利用這幾個方案進(jìn)行消息轉(zhuǎn)發(fā);

注意, 前一套方案實(shí)現(xiàn), 后一套方法就不會執(zhí)行伏尼。

如果這幾套方案你都沒有做處理, 那么 objc_msgSend() 最后找不到合適的方法進(jìn)行調(diào)用, 會報錯 unrecognized selector sent to instance

方案1 (動態(tài)方法解析, 添加方法):

// 首先烦粒,系統(tǒng)會調(diào)用resolveInstanceMethod或resolveClassMethod 讓你自己為這個方法動態(tài)增加實(shí)現(xiàn)扰她。動態(tài)添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

方案2 (消息轉(zhuǎn)發(fā)):

// 如果不對resolveInstanceMethod做任何處理, 系統(tǒng)會來到方案二, 走forwardingTargetForSelector方法徒役,我們可以返回其他實(shí)例對象, 實(shí)現(xiàn)消息轉(zhuǎn)發(fā)忧勿。
- (id)forwardingTargetForSelector:(SEL)aSelector;

方案3 (消息轉(zhuǎn)發(fā)調(diào)用):

// 如果不實(shí)現(xiàn) forwardingTargetForSelector, 系統(tǒng)就會調(diào)用方案三的兩個方法 方法簽名&轉(zhuǎn)發(fā)調(diào)用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
流程圖示
image

消息發(fā)送 ??

查找方法的執(zhí)行流程

圖示

image

源碼解讀

objc_msgSend()

因?yàn)檫@個方法調(diào)用頻次太高, 所以使用匯編實(shí)現(xiàn)提高效率

// 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是跳轉(zhuǎn); 如果 le (p0≤0)成立, 則跳轉(zhuǎn)到 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

// 消息接收者為空, 則直接退出函數(shù)
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 調(diào)用了 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 開頭的指令一般是跳轉(zhuǎn)調(diào)用
    // c函數(shù)在編譯成匯編之后, 會多一條下劃線, 找到c的 lookUpImpOrForward, 返回的是函數(shù)地址 IMP ★
    // 傳入的參數(shù) 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() 調(diào)用

// objc-runtime-new.mm
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    // 消息轉(zhuǎn)發(fā)
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup  樂觀(嘗試)查找一下緩存, 因?yàn)檫@期間有可能別的地方有調(diào)用了這個方法, 添加到了緩存
    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();
    // 遍歷用的`游標(biāo)`(類/元類)
    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().
    // 如果當(dāng)前類的緩存中沒有, 則遍歷, 根據(jù)superclass指針向上一層層查找 ★★
    for (unsigned attempts = unreasonableClassCount();;) {
        
        // 根據(jù)方法名 sel 去當(dāng)前`游標(biāo)`中查找方法 ★ 
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel); // ★
        if (meth) {
            // 取出函數(shù)地址 IMP
            imp = meth->imp;
            goto done;
        }
        // 游標(biāo)向父類步進(jìn), 如果直到根類都沒找到, 進(jìn)入消息轉(zhuǎn)發(fā) ★
        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)) {
            // 從父類中找到了方法, 填充到當(dāng)前類的緩存 ★★
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
    
    // No implementation found. Try method resolver once.
    // 首次查找時, 沒找到, 嘗試一次動態(tài)方法解析 ★★★
    // 此時, behavior = 0b0011; &= LOOKUP_RESOLVER 為 0b0010, 可以進(jìn)入if(); 
    
    // 由于 resolveMethod_locked 內(nèi)部調(diào)用了本函數(shù), 而如果 resolveMethod_locked 并沒有動態(tài)添加方法, 會再次來到這里
    // 此時, behavior = 0b0101; &= LOOKUP_RESOLVER 為 0b0000, 不能進(jìn)入if();
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // behavior = 0b0011 ^ 0b0010 = 0b0001
        behavior ^= LOOKUP_RESOLVER;
        // 該函數(shù)內(nèi)部會回調(diào)本函數(shù), 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()

根據(jù)方法名 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 (二維數(shù)組) ★
    auto const methods = cls->data()->methods();
    // 遍歷二維數(shù)組, 找到 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 中被調(diào)用

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
        // 調(diào)用 cache_t::insert 將新調(diào)用的方法插入到當(dāng)前類對象的方法緩存中 ★
        cache->insert(cls, sel, imp, receiver);
    }
#else
    _collecting_in_critical();
#endif
}

動態(tài)方法解析 ??

圖示

image

源碼解讀

resolveMethod_locked()

在 lookUpImpOrForward() 中有調(diào)用

static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) { // 類對象, 實(shí)例方法 ★
        // 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);
        }
    }
    
    // 再次調(diào)用 `lookUpImpOrForward()` 入?yún)?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 發(fā)送 `resolveInstanceMethod:` ★ 
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    
    // 這個返回值只是做了一些打印, 所以 `resolveInstanceMethod:` 的返回值為 YES / NO 實(shí)際效果都一樣
    // `resolveInstanceMethod:` 不添加方法實(shí)現(xiàn), 僅僅返回 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);
        }
    }
    // 讓元類調(diào)用 `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));
        }
    }
}

動態(tài)添加方法

開發(fā)者可以實(shí)現(xiàn)以下方法, 來動態(tài)添加方法實(shí)現(xiàn)

+ (BOOL)resolveInstanceMethod:(SEL)sel;

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        // 動態(tài)添加test方法的實(shí)現(xiàn), 將方法添加到 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)) {
        // 第一個參數(shù)是object_getClass(self) 元類對象
        class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

動態(tài)解析過后, 會重新走“消息發(fā)送”的流程

<u>“從receiverClass 的 cache中查找方法”這一步開始執(zhí)行</u>

消息轉(zhuǎn)發(fā) ??

自己及父類都無法處理該消息, 也沒有動態(tài)方法解析, 會進(jìn)入消息轉(zhuǎn)發(fā)階段, 將消息轉(zhuǎn)發(fā)給其他實(shí)例 /類 (備用接收者)

注意, 消息機(jī)制支持, 類方法, 實(shí)例方法和類方法本質(zhì)沒有區(qū)別, 都是消息機(jī)制谆膳。

當(dāng)對象不能接受某個selector時, 如果不對 resolveInstanceMethod 做任何處理, 系統(tǒng)會來到 forwardingTargetForSelector 方法, 我們可以返回其他實(shí)例對象, 實(shí)現(xiàn)消息轉(zhuǎn)發(fā)。

圖示

image

使用

forwardingTargetForSelector
// 對于類方法的轉(zhuǎn)發(fā)
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    // 這里甚至可以返回實(shí)例對象, 轉(zhuǎn)發(fā)給實(shí)例對象來調(diào)用, 相當(dāng)于:
    // 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)) {
        // 返回一個實(shí)現(xiàn)了該方法的對象, 實(shí)際相當(dāng)于調(diào)用了下面的方法
        // `objc_msgSend([[Animal alloc] init], aSelector)`
        return [[Animal alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 若返回的對象如果沒有實(shí)現(xiàn)該方法, 相當(dāng)于返回了nil 
  • 若實(shí)現(xiàn)了該方法, 且返回值不空, 則將消息轉(zhuǎn)發(fā)給其他對象

  • 若未實(shí)現(xiàn)該方法, 或者返回了nil, 會調(diào)用 methodSignatureForSelector:, 要求返回方法簽名

methodSignatureForSelector
// 方法簽名:返回值類型、參數(shù)類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        // 手寫方法簽名
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
        // 也可以讓一個實(shí)現(xiàn)了該方法的對象, 調(diào)用本方法, 并返回結(jié)果
        // return [[[Xxx alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
  • 若實(shí)現(xiàn)了該方法, 且返回值不空, 則將其返回的方法簽名封裝到 NSInvocation,并調(diào)用 forwardInvocation: ;

    其中, 方法簽名決定了 NSInvocation 方法的返回值及參數(shù)數(shù)量、類型晃危。

  • 若未實(shí)現(xiàn)該方法, 或者返回了nil, 會調(diào)用 doesNotRecognizeSelector:, 直接報找不到方法; ★

forwardInvocation
/*
* NSInvocation 封裝了一個方法調(diào)用, 包括: 方法調(diào)用者僚饭、方法名胧砰、方法參數(shù)
* anInvocation.target 方法調(diào)用者, 默認(rèn)是最開始接收消息的對象
* anInvocation.selector 方法名
* [anInvocation getArgument:NULL atIndex:0] 調(diào)用時傳入的參數(shù)
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//    anInvocation.target = [[Cat alloc] init];
//    [anInvocation invoke];
 
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
}
  • 該方法內(nèi), 不是必須調(diào)用 invoke,

  • 實(shí)際上這個方法內(nèi)部是可以做任何事, 甚至不做任何事;
    比如可以修改參數(shù), 修改返回值等等;

NSMethodSignature 顧名思義應(yīng)該就是“方法簽名”, 類似于C++中的編譯器時的函數(shù)簽名偿乖。蘋果官方定義該類為對方法的參數(shù)哲嘲、返回類似進(jìn)行封裝, 協(xié)同NSInvocation實(shí)現(xiàn)消息轉(zhuǎn)發(fā)眠副。通過消息轉(zhuǎn)發(fā)實(shí)現(xiàn)類似C++中的多重繼承囱怕。

iOS中的SEL, 它的作用和C光涂、C++中的函數(shù)指針很相似, 通過performSelector:withObject:函數(shù)可以直接調(diào)用這個消息。

但是perform相關(guān)的這些函數(shù), 有一個局限性, 其參數(shù)數(shù)量不能超過2個, 否則要做很麻煩的處理, 與之相對, NSInvocation也是一種消息調(diào)用的方法, 并且它的參數(shù)沒有限制钝计。這兩種直接調(diào)用對象消息的方法, 在IOS4.0之后, 大多被block結(jié)構(gòu)所取代, 只有在很老的兼容性系統(tǒng)中才會使用私恬。

源碼解讀

__objc_msgForward_impcache

lookUpImpOrForward() 中調(diào)用

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

這里沒有開源, 只有寫打印信息; 接下來就需要逆向分析匯編代碼了;

先考慮換一種思路, 如果不實(shí)現(xiàn)消息轉(zhuǎn)發(fā), Xcode崩潰會輸出函數(shù)調(diào)用棧:

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___ 會調(diào)用 forwardingTargetForSelector:

// 國外開發(fā)者根據(jù)匯編寫出的偽代碼
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);

    // 調(diào)用 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:` 沒有實(shí)現(xiàn), 或者返回了nil; 會調(diào)用 `methodSignatureForSelector:`, 要求返回方法簽名
    // 調(diào)用 methodSignatureForSelector 獲取方法簽名后, 再調(diào)用 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 是否已經(jīng)在 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);
}

消息轉(zhuǎn)發(fā)的應(yīng)用

  • 容災(zāi)處理: 防止找不到方法, 產(chǎn)生崩潰
@implementation Person
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 本來能調(diào)用的方法
    if ([self respondsToSelector:aSelector]) {
        return [super methodSignatureForSelector:aSelector];
    }
    // 找不到的方法
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 找不到的方法, 都會來到這里
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 可以在這里收集找不到的方法
    NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
}
@end
  • NSProxy 專門用來做消息轉(zhuǎn)發(fā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鲤拿,隨后出現(xiàn)的幾起案子署咽,更是在濱河造成了極大的恐慌宁否,老刑警劉巖慕匠,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件絮重,死亡現(xiàn)場離奇詭異,居然都是意外死亡督怜,警方通過查閱死者的電腦和手機(jī)号杠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門姨蟋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來眼溶,“玉大人晓勇,你說我怎么就攤上這事绑咱∶枞冢” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵毛甲,是天一觀的道長敞恋。 經(jīng)常有香客問我硬猫,道長啸蜜,這世上最難降的妖魔是什么辈挂? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任蜂林,我火速辦了婚禮拇泣,結(jié)果婚禮上霉翔,老公的妹妹穿的比我還像新娘债朵。我一直安慰自己,他們只是感情好臭杰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布渴杆。 她就那樣靜靜地躺著将塑,像睡著了一般点寥。 火紅的嫁衣襯著肌膚如雪来吩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天盗冷,我揣著相機(jī)與錄音仪糖,去河邊找鬼锅劝。 笑死蟆湖,一個胖子當(dāng)著我的面吹牛隅津,可吹牛的內(nèi)容都是我干的伦仍。 我是一名探鬼主播呢铆,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼棺克,長吁一口氣:“原來是場噩夢啊……” “哼娜谊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起湾趾,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤搀缠,失蹤者是張志新(化名)和其女友劉穎艺普,沒想到半個月后歧譬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瑰步,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缩焦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年舌界,在試婚紗的時候發(fā)現(xiàn)自己被綠了呻拌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睦焕。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡猾普,死狀恐怖本谜,靈堂內(nèi)的尸體忽然破棺而出乌助,到底是詐尸還是另有隱情他托,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站纫溃,受9級特大地震影響紊浩,放射性物質(zhì)發(fā)生泄漏郎楼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虹钮。 院中可真熱鬧膘融,春花似錦氧映、人聲如沸岛都。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸽斟。三九已至湾盗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躏吊,已是汗流浹背比伏。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工赁项, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悠菜,地道東北人悔醋。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓芬骄,卻偏偏與公主長得像账阻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姻僧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容