OC方法查找(消息發(fā)送)流程

認識runtime

參考官方文檔

objc_msgSend

OC方法其本質上就是通過調用objc_msgSend函數(shù)來發(fā)送消息载绿,objc_msgSend函數(shù)的聲明如下:

id objc_msgSend(id self, SEL _cmd, ...);

objc_msgSend函數(shù)有有兩個重要的參數(shù)self和_cmd瓷式,self就是消息接收者循捺,_cmd就是方法,也就是消息须蜗。objc_msgSend函數(shù)里面會通過self的isa指針找到對應的類,從類結構中查詢方法實現(xiàn)進行執(zhí)行躏结。這里附上一張isa指針流程圖以及詳細分析文章鏈接(點擊了解isa及其詳細走位流程):

isa流程圖.png

快速查找(緩存)

objc_msgSend代碼是由匯編實現(xiàn)的劫狠,其匯編代碼如下:

    ENTRY _objc_msgSend //進入_objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check, p0=self
#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
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
......這里省略 TAGGED_POINTERS相關的流程,因為當前不做分析
// 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

判斷不是targed_pointer類型后骚揍,調用宏定義函數(shù)GetClassFromIsa_p16通過將isa指針進行一系列指針平移字管,最后和掩碼進行與操作得到Class指針:

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK //與 ISA_MASK進行&操作讀取Class指針

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

拿到Class之后,接著就通過函數(shù)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

其大概邏輯是通過指針平移找到類的cache_t cache(點擊了解cache_t)嘲叔,然后再利用bucketsMask和_maskAndBuckets通過位運算得到相應的buckets指針,然后再通過SEL _cmd和對應的mask做位運算得到一個index抽活,先通過index在buckets中讀取出sel硫戈,如果跟當前的_cmd一致那就返回,如果不一致就跳到buckets的末尾下硕,通過指針平移從后往前查找(可能是因為存的時候大方向是從前往后的丁逝,當然不一定是按順序存儲),找到(CacheHit)返回imp梭姓。找不到(CheckMiss)就調用函數(shù)__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

函數(shù)__objc_msgSend_uncached的實現(xiàn)實際上就調用了MethodTableLookup函數(shù):

.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

MethodTableLookup函數(shù)中一大堆實現(xiàn)也不知道干嘛霜幼,但是有一個很重要的信息就是調用了lookUpImpOrForward函數(shù),這其實就是進入了方法慢速查找流程誉尖。

慢速方法查找流程

當緩存中找不到方法時就會進入慢速查找過程罪既,實際上就是調用lookUpImpOrForward函數(shù),其代碼如下:

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
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 lookpu 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().

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            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.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        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;
}

函數(shù)進來之后再次執(zhí)行快速查找流程铡恕,這一步主要考慮到多線程的原因有可能在這段時間前有方法被緩存了琢感,找到就返回,找不到就繼續(xù)往下探熔。這期間還有各種校驗驹针,包括類是否已初始化等校驗。然后才進行如下查找流程:

1诀艰、通過當前類結構體查找方法列表:

首先通過調用getMethodNoSuper_nolock從當前類的方法列表中查找:

Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

函數(shù)getMethodNoSuper_nolock通過當前類獲取方法列表 cls->data()->methods()柬甥,然后交由函數(shù)search_method_list_inline通過調用函數(shù)findMethodInSortedMethodList進行二分法查找

/***********************************************************************
 * getMethodNoSuper_nolock
 * fixme
 * Locking: runtimeLock must be read- or write-locked by the caller
 **********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    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;
}

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

在函數(shù)findMethodInSortedMethodList中進行二分法查找墙牌。因為方法列表首先是個數(shù)組,而且是個有序數(shù)組暗甥,它是根據(jù)method_t的SEL的地址大小進行排序的喜滨,所以可以進行二分法查找:

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
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);//右移一位實際上就是除以2,這一語句相當于指針往前走到中間位置撤防,這里是二分法
        
        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--;//這里減減操作是考慮多個SEL同名虽风,優(yōu)先調用最前面的一個,比如category方法寄月,運行時會添加到函數(shù)列表前面辜膝,因此會被優(yōu)先查詢到
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

如果找到就會返回imp,并且會把sel和imp都緩存到cache里面漾肮。

2厂抖、找到并寫入緩存:
 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
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);//在這方法里面里會調用insert方法進行寫入緩存
}

最后會在方法cache_fill(cls, sel, imp, receiver)里面里會調用insert方法進行寫入緩存(點擊了解詳情);
如果找不到就往下對父類進行遞歸查找。

3克懊、遞歸地從父類查找

首先通過curClass = curClass->superclass獲取當前類的父類忱辅,然后賦值給curClass,之后就會先獲取父類緩存谭溉,找到就返回墙懂,找不到繼續(xù)進行for循環(huán)尋找父類方法列表:

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            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;
        }
    }

如果父類找不到,基類也找不到扮念,也就是superclass=nil時损搬,這時候imp會被賦予一個默認值forward_imp,這個forward_imp在函數(shù)開始時就有賦值了:

4柜与、繼承鏈中找不到方法時
const IMP forward_imp = (IMP)_objc_msgForward_impcache;

那這個_objc_msgForward_impcache到底是什么東西呢巧勤?經(jīng)過跟蹤發(fā)現(xiàn)它是由匯編實現(xiàn)的:

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward //調用函數(shù)__objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward   //開始調用__objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17 //調用函數(shù)__objc_forward_handler
    
    END_ENTRY __objc_msgForward  //結束調用__objc_msgForward

根據(jù)代碼分析,其最終調用的是函數(shù)_objc_forward_handler弄匕,它的實現(xiàn)代碼如下:

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

這其實就是我們平時調用不存在的方法時發(fā)生崩潰的并打印崩潰信息“....unrecognized selector sent to instance......”的地方颅悉。
當然,到基類這一步方法如果還沒有找到粘茄,它其實不會立即執(zhí)行_objc_forward_handler這個函數(shù)签舞。蘋果還給我們提供了補救的方案。那就是接下來的動態(tài)方法決議和消息轉發(fā)流程柒瓣。

5、動態(tài)方法決議

遞歸到基類再找不到就會跳出for循環(huán)吠架,并進入動態(tài)方法決議:

 // No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

從resolveMethod_locked函數(shù)的實現(xiàn)可以看出芙贫,動態(tài)方法決議中對象方法和類方法的處理流程是不一樣的:

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

    runtimeLock.unlock();

    if (! cls->isMetaClass()) { //cls不是元類,說不是類方法傍药,是對象方法 
        // 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);
}

動態(tài)決議執(zhí)行完成之后又會調用一遍lookUpImpOrForward磺平。接下來看一下對象方法動態(tài)決議過程魂仍。

  • 對象方法動態(tài)決議
    當判斷是對象方法時,會直接調用resolveInstanceMethod函數(shù):
/***********************************************************************
* 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;
    }

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

這時就會首先判斷我的cls有沒有實現(xiàn)方法+ (BOOL)resolveInstanceMethod:(SEL)sel拣挪,如果沒有實現(xiàn)就直接return擦酌。如果實現(xiàn)了,那么就會通過objc_msgSend像cls發(fā)送resolve_sel消息菠劝,實際上就是調用resolveInstanceMethod方法赊舶,調用這個方法同樣也會走一遍消息發(fā)送流程,如果這個cls或者其父類實現(xiàn)resolveInstanceMethod方法赶诊,并對sel進行處理笼平,比如當我們調用一個沒有實現(xiàn)的方法noMethod時,可以再resolveInstanceMethod中進行攔截舔痪,并添加一個新的方法寓调,如下:

- (void)instanceMethod{
    NSLog(@"這是實例方法");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(noMethod)) {
        IMP imp           = class_getMethodImplementation(self, @selector(instanceMethod));
        Method aMethod = class_getInstanceMethod(self, @selector(instanceMethod));
        const char *type  = method_getTypeEncoding(aMethod);
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

這樣當函數(shù)static void resolveInstanceMethod(id inst, SEL sel, Class cls)中再次調用 IMP imp = lookUpImpOrNil(inst, sel, cls) 時就能找到并調用IPM,這個IPM就是instanceMethod锄码。

  • 類方法動態(tài)決議
    如果是類方法夺英,則調用方法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);
        }
    }
    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));
        }
    }
}

類方法其實跟對象方法很相似滋捶,就是調用resolveClassMethod換成resolveInstanceMethod秋麸,處理流程是差不多的。這里就不進行分析炬太。主要不同的是灸蟆,類方法調用完resolveClassMethod之后,還會進行一次lookUpImpOrNil亲族,并在此調用resolveInstanceMethod炒考,這又是為什么呢?

 resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }

實際上這跟isa的走位有關(點擊了解isa走位流程)霎迫,雖然代碼的外部結構我看到的是類方法斋枢,但實際上類方法本身它也是元類的對象方法,方法存儲在元類中知给,而元類的基類又是NSObject瓤帚,因此還需要調用一次對象方法動態(tài)決議。舉個例子涩赢,加入我們在NSObject的category里面實現(xiàn)了對象方法決議戈次,如下demo:

@implementation NSObject (Test)

- (void)instanceMethod{
    NSLog(@"這是實例方法");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(noMethod)) {
        IMP imp           = class_getMethodImplementation(self, @selector(instanceMethod));
        Method aMethod = class_getInstanceMethod(self, @selector(instanceMethod));
        const char *type  = method_getTypeEncoding(aMethod);
        return class_addMethod(self, sel, imp, type);
    }
    
    return NO;
}

@end

然后我們從NSObject的一個子類MyObject調用一個不存在的類方法:

    [MyObject performSelector:@selector(noMethod)];

最終也會調用到NSObject的resolveInstanceMethod方法里面,接受處理筒扒。
動態(tài)方法決議只是給我們提供了一個補救的方式處理未實現(xiàn)的方法調用(當然實際開發(fā)中一般不在NSObject這做處理怯邪,影響比較大)。但是如果我們沒有實現(xiàn)動態(tài)決議相關的處理的話花墩,那么消息發(fā)送路程就會進入消息轉發(fā)流程悬秉。

6澄步、消息轉發(fā)流程

消息轉發(fā)流程包括兩個步驟,快速轉發(fā)和慢速轉發(fā)和泌。

  • 快速轉發(fā)
    進入快速轉發(fā)流程實際上是調用了forwardingTargetForSelector方法村缸,通過這個方法返回一個指定的對象負責處理這個消息,舉一個例子:
@interface MyObject1 : NSObject

@end

@implementation MyObject1

- (void)noMethod
{
    NSLog(@"這是MyObject1的方法");
}

@end


@interface MyObject : NSObject
- (void)instanceMethod;
+ (void)classMethod;
@end

@implementation MyObject

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(noMethod)) {
        return [[MyObject1 alloc] init];
    }
    return nil;
}
}

demo中MyObject實現(xiàn)了forwardingTargetForSelector方法武氓,并且判斷如果是調用noMethod方法的時候就轉發(fā)給MyObject1的對象去處理梯皿。比如進行如下調用:

 MyObject *objc = [[MyObject alloc] init];
    [objc performSelector:@selector(noMethod)];

那么這段代碼最終會執(zhí)行MyObject1的noMethod方法。因為這個轉發(fā)直接指定一個接收轉發(fā)消息的對象聋丝,所以叫快速轉發(fā)索烹。注意,這一段轉發(fā)之后弱睦,MyObject1的對象在調用noMethod方法時也是走一遍消息發(fā)送流程的百姓。如果在這一層沒有處理的話,那么消息發(fā)送就會進入慢速轉發(fā)流程况木。

  • 慢速轉發(fā)
    慢速轉發(fā)流程會涉及到兩個方法垒拢。如果我們想在這一層處理這一消息需要實現(xiàn)兩個方法forwardInvocation和methodSignatureForSelector。首先我們需要在methodSignatureForSelector中創(chuàng)建一個方法簽名火惊,例如上面demo中的noMethod方法可以這樣處理:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    return methodSignature;
}

創(chuàng)建這個簽名methodSignature并返回之后會繼續(xù)調用forwardInvocation(實際上在調用forwardInvocation之前還做了一次動態(tài)方法決議)求类,這里面涉及到一個很重要的類NSInvocation,NSInvocation的定義如下:

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

其屬性methodSignature就是保存了我們剛才創(chuàng)建的簽名屹耐,還有兩個重要屬性target和selector尸疆,selector就是調用的noMethod方法,target就是調用這個方法的對象惶岭。NSInvocation中還有兩個方法invoke和invokeWithTarget寿弱,執(zhí)行invoke就是讓當前target調用selector,而invokeWithTarget則可以指定新的target調用selector按灶,比如調用noMethod我們可以這樣處理:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:[[MyObject1 alloc] init]];
}

這里我們創(chuàng)建MyObject1的對象并指定為noMethod的調用者症革。注意:在這一步雖然是通過invoke調用,但是實際上最后也是向target對象發(fā)送消息鸯旁,依然會走消息發(fā)送的流程噪矛。
其實到我們實現(xiàn)了- (void)forwardInvocation:(NSInvocation *)invocation這個方法的時候,只要methodSignatureForSelector返回不為nil铺罢,程序不會因此而崩潰艇挨,如果methodSignatureForSelector返回為nil將不會再調用forwardInvocation方法而直接調用前文提到的默認imp,然后調用方法doesNotRecognizeSelector畏铆,最后程序崩潰雷袋。
慢速轉發(fā)的優(yōu)點
在forwardInvocation方法中我們可以選擇做很多事情,相比于快速轉發(fā)的方法forwardingTargetForSelector有很一些優(yōu)點:

1辞居、不止可以指定新的target楷怒,還可以修改selector;
2瓦灶、invokation對象可以緩存鸠删,在合適的時機調用(invoke);

總結

1贼陶、通過isa指針獲取當前類結構刃泡,在類結構緩存中進行快速查找,命中緩存則直接返回碉怔,沒有命中執(zhí)行2步驟烘贴;
2、通過當前類結構尋找方法列表撮胧,在方法列表中查找桨踪,找不到執(zhí)行3步驟,找到就執(zhí)行7步驟芹啥;
3锻离、遞歸尋找父類的緩存和方法列表(實際是調用步驟1),找不到就就會設置一個默認的執(zhí)行函數(shù)墓怀,但不返回汽纠,先執(zhí)行步驟4,如果找到就執(zhí)行步驟7傀履;
4虱朵、進入方法決議,方法決議沒有處理就執(zhí)行步驟4钓账;
5碴犬、就進入消息轉發(fā)流程,消息轉發(fā)流程如果沒有處理就會執(zhí)行步驟6 官扣。
6翅敌、調用默認函數(shù),程序崩潰并打印方法無法找到的崩潰信息
7惕蹄、找到方法就返回蚯涮,同時寫入緩存。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末卖陵,一起剝皮案震驚了整個濱河市遭顶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泪蔫,老刑警劉巖棒旗,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡铣揉,警方通過查閱死者的電腦和手機饶深,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逛拱,“玉大人敌厘,你說我怎么就攤上這事⌒嗪希” “怎么了俱两?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長曹步。 經(jīng)常有香客問我宪彩,道長,這世上最難降的妖魔是什么讲婚? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任尿孔,我火速辦了婚禮,結果婚禮上磺樱,老公的妹妹穿的比我還像新娘纳猫。我一直安慰自己,他們只是感情好竹捉,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布芜辕。 她就那樣靜靜地躺著,像睡著了一般块差。 火紅的嫁衣襯著肌膚如雪侵续。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天憨闰,我揣著相機與錄音状蜗,去河邊找鬼。 笑死鹉动,一個胖子當著我的面吹牛轧坎,可吹牛的內容都是我干的。 我是一名探鬼主播泽示,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼缸血,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了械筛?” 一聲冷哼從身側響起捎泻,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎埋哟,沒想到半個月后笆豁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年闯狱,在試婚紗的時候發(fā)現(xiàn)自己被綠了煞赢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡扩氢,死狀恐怖耕驰,靈堂內的尸體忽然破棺而出爷辱,到底是詐尸還是另有隱情录豺,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布饭弓,位于F島的核電站双饥,受9級特大地震影響,放射性物質發(fā)生泄漏弟断。R本人自食惡果不足惜咏花,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阀趴。 院中可真熱鬧昏翰,春花似錦、人聲如沸刘急。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叔汁。三九已至统求,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間据块,已是汗流浹背码邻。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留另假,地道東北人像屋。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像边篮,于是被迫代替她去往敵國和親己莺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內容