objc_msgSend流程分析

前言

我们知道OC的上层方法调用时,在底层都会转化为objc_msgSend调用,那么它的流程是怎么样的呢,我们又如何理解它, 我们带着这些疑问来分析objc_msgSend。
首先我们先难证一下,OC的上层方法是不是会转化成objc_msgSend,如下图:

1

2

3

我们定义了一个RoPerson类的对象并调用了saySomething,通过汇编我们可以清楚的看到是转化为了objc_msgSend调用,这也就说明了OC上层的方法会被转化为objc_msgSend调用,并且是在libobjc.A.dylib这个动态库中,objc_msgSend是有汇编写的。

objc_msg_Send汇编分析

我们在objc(818版本)的源码中找到objc_msg_Send的代码,如代码所示:

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check,判断当前的消息接受者是否为0,如果没有消息接受者,就无意义了
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa,把当前的isa给到p13寄存器
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class,p13就是isa,x0也就isa
LGetIsaDone: // 这里获取isa已经完成,开始执行下一步操作
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

// 这几行代码如果是*SUPPORT_TAGGED_POINTERS*(后续补充)执行*b.le   LNilOrTagged*,否则执行*b.eq LReturnZero*。
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone

我们接着分析 GetClassFromIsa_p16这个函数,看看他到底做了什么,代码如下:

// p13(isa)也就是src参数, 1是needs_auth参数, x0(isa)是auth_address参数
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, \src           // 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
  // 这里不是index isa,所以不执行,直接执行1
1:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already,needs_auth传过来的是1,所以下面一行代码不执行,执行.else的代码
    mov p16, \src

.else
    // 64-bit packed isa
    ExtractISA p16, \src, \auth_address  // p16是一个空的的地址,src,auth_address都是isa
.endif
#else
    // 32-bit raw isa
    mov p16, \src

#endif

.endmacro

我们再看下ExtractISA这个函数的流程执行,全局搜下:

// p16 = isa & ISA_MASK
.macro ExtractISA
    and    $0, $1, #ISA_MASK  // 这行代码就是, $1 逻辑与(按位)ISA_MASK,然后给到$0 也就是p16,and是逻辑与,
.endmacro

这也就是解释了p16 就是class

接着我们再来分析CacheLookup,我们搜下它的宏:

// Mode就是NORMAL, Function就是_objc_msgSend,__objc_msgSend_uncached, MissLabelDynamic ,  MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart\Function 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\Function,
    //   then our PC will be reset to LLookupRecover\Function 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
    //
    
    mov x15, x16            // stash the original isa,这里x16就是p16,把x16移到x15寄存器中
LLookupStart\Function: // 开始找_objc_msgSend流程
    // p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    ldr p10, [x16, #CACHE]              // p10 = mask|buckets
    lsr p11, p10, #48           // p11 = mask
    and p10, p10, #0xffffffffffff   // p10 = buckets
    and w12, w1, w11            // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 真机的架构。
    ldr p11, [x16, #CACHE]          // p11 = mask|buckets,把x16的地址平移CACHE大小,经过全局搜索CACHE是16字节,就是平移到cahce结构体的位置,这个时候p11=cache_t。
    #if CONFIG_USE_PREOPT_CACHES // 这里从来的没找过,所以不执行,我们可以看下它的else,CONFIG_USE_PREOPT_CACHES这个值为1,可以全局搜下
        #if __has_feature(ptrauth_calls)
    tbnz    p11, #0, LLookupPreopt\Function
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
        #else
    and p10, p11, #0x0000fffffffffffe   // p10 = buckets,这里就是p11 & 0x0000fffffffffffe(掩码),并赋给10寄存器
    tbnz    p11, #0, LLookupPreopt\Function,p11与0做比较,如果p11不为0,跳转到LLookupPreopt。
        #endif
    eor p12, p1, p1, LSR #7 // p1右移7位存到p12中
    and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else

//  p11 cache -> p10 = buckets
//  p11, LSR #48 -> mask, 
//  p1(_cmd) &  mask = index -> p12
    and p10, p11, #0x 000 0ffffffffffff // p10 = buckets
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask,p11 右移48位得到mask值,然后与p1(sel也就是_cmd)进行&运算,得到index值,并赋给p12寄存器。

#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
    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

// objc - 源码调试 + 汇编
//  p11 cache -> p10 = buckets
//  p1(_cmd) & mask = index -> p12
//  (_cmd & mask) << 4 (左移4位),buckets+就是内存平移,就是平移int(占用4字节)的大小 
//  buckets  +  内存平移 (1 2 3 4)
//  b[i] -> b + i
//  p13 当前要查找的bucket
//  PTRSHIFT=3
    add p13, p10, p12, LSL #(1+PTRSHIFT)
                        // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),

                        // do {
//  *bucket--  p17, p9
//  bucket 里面的东西 imp (p17) sel (p9)
//  查到的 sel (p9) 和我们(要查的) say1
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--,x13往上移BUCKET_SIZE这个大小,存到p9(sel),p17(imp)这个位置
    cmp p9, p1              //     if (sel != _cmd) { 
    b.ne    3f              //         scan more,如果不等于,执行3,继续查找
                        //     } else {
2:  CacheHit \Mode              // hit:    call or return imp,缓存命中(跳转或者返回imp)
                        //     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
    b.hs    1b // 跳转到1,继续执行

    // wrap-around:
    //   p10 = first bucket
    //   p11 = mask (and maybe other bits o n LP64)
    //   p12 = _cmd & mask
    //
    // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
    // So stop when we circle back to the first probed bucket
    // rather than when hitting the first bucket again.
    //
    // Note that we might probe the initial bucket twice
    // when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                        // p13 = buckets + (mask << 1+PTRSHIFT)
                        // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p13, p10, p11, LSL #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                        // p12 = first probed bucket

                        // do {
4:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel == _cmd)
    b.eq    2b              //         goto hit
    cmp p9, #0              // } while (sel != 0 &&
    ccmp    p13, p12, #0, ne        //     bucket > first_probed)
    b.hi    4b

LLookupEnd\Function:
LLookupRecover\Function:
    b   \MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
    and p10, p11, #0x007ffffffffffffe   // p10 = buckets
    autdb   x10, x16            // auth as early as possible
#endif

    // x12 = (_cmd - first_shared_cache_sel)
    adrp    x9, _MagicSelRef@PAGE
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    sub p12, p1, p9

    // w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    lsr x17, x11, #55           // w17 = (hash_shift, ...)
    lsr w9, w12, w17            // >>= shift

    lsr x17, x11, #60           // w17 = mask_bits
    mov x11, #0x7fff
    lsr x11, x11, x17           // p11 = mask (0x7fff >> mask_bits)
    and x9, x9, x11         // &= mask
#else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr x17, x11, #48           // w17 = (hash_shift, hash_mask)
    lsr w9, w12, w17            // >>= shift
    and x9, x9, x11, LSR #53        // &=  mask
#endif

    ldr x17, [x10, x9, LSL #3]      // x17 == sel_offs | (imp_offs << 32)
    cmp x12, w17, uxtw

.if \Mode == GETIMP
    b.ne    \MissLabelConstant      // cache miss
    sub x0, x16, x17, LSR #32       // imp = isa - imp_offs
    SignAsImp x0
    ret
.else
    b.ne    5f              // cache miss
    sub x17, x16, x17, LSR #32      // imp = isa - imp_offs
.if \Mode == NORMAL
    br  x17
.elseif \Mode == LOOKUP
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    SignAsImp x17
    ret
.else
.abort  unhandled mode \Mode
.endif

5:  ldursw  x9, [x10, #-8]          // offset -8 is the fallback offset
    add x16, x16, x9            // compute the fallback isa
    b   LLookupStart\Function       // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

这里我们分析下LLookupPreopt这个函数,搜到如下:

LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
    and p10, p11, #0x007ffffffffffffe   // p10 = buckets, 这里是p11寄存器逻辑与(按位)0x007ffffffffffffe,给到p10,也就是说p10=buckets。
    autdb   x10, x16            // auth as early as possible
#endif

    // x12 = (_cmd - first_shared_cache_sel)
    adrp    x9, _MagicSelRef@PAGE
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    sub p12, p1, p9

    // w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    lsr x17, x11, #55           // w17 = (hash_shift, ...)
    lsr w9, w12, w17            // >>= shift

    lsr x17, x11, #60           // w17 = mask_bits
    mov x11, #0x7fff
    lsr x11, x11, x17           // p11 = mask (0x7fff >> mask_bits)
    and x9, x9, x11         // &= mask
#else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr x17, x11, #48           // w17 = (hash_shift, hash_mask)
    lsr w9, w12, w17            // >>= shift
    and x9, x9, x11, LSR #53        // &=  mask
#endif

    ldr x17, [x10, x9, LSL #3]      // x17 == sel_offs | (imp_offs << 32)
    cmp x12, w17, uxtw

.if \Mode == GETIMP
    b.ne    \MissLabelConstant      // cache miss
    sub x0, x16, x17, LSR #32       // imp = isa - imp_offs
    SignAsImp x0
    ret
.else
    b.ne    5f              // cache miss
    sub x17, x16, x17, LSR #32      // imp = isa - imp_offs
.if \Mode == NORMAL
    br  x17
.elseif \Mode == LOOKUP
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    SignAsImp x17
    ret
.else
.abort  unhandled mode \Mode
.endif

5:  ldursw  x9, [x10, #-8]          // offset -8 is the fallback offset
    add x16, x16, x9            // compute the fallback isa
    b   LLookupStart\Function       // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

我们再来看下CacheHit这个函数,代码如下:

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x10, x1, x16    // authenticate imp and re-sign as IMP
    cmp x16, x15
    cinc    x16, x16, ne            // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

TailCallCachedImp这个函数,就是对imp进行编码,并跳转到imp

慢速查找流程

慢述查找的基本流程是:
1 查自己的methodlist,如果查找到sel和imp,返回
2 如果没查找到,去查找父类的methoslist,如果查找到sel和imp,返回
3 接着去查找NSObject,如果还是没有查到,就跳转出,下面我们来验证一下。

慢速查找流程准备

我们在objc的源码中搜下lookUpImpOrForward这个C++函数,我们把代码贴出来分析下:

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

    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // 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.
    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    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().

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == 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:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;

我们对这个源码分析一下
checkIsKnownClass(cls);检查当前的class是否已经注册到当前的缓存中。
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);对rw,ro的一些处理以及isa走位。

二分查找

上述的代码片段中

#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif

这是从共享缓存中查找,我们在处理rw,ro,methodslist的时候,这个时候有可能正好写入了方法,所以我们再查找一遍。
接下来,我们来看下**getMethodNoSuper_nolock这个函数的实现流程:

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

我们再来看下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->isExpectedSize();

if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    return findMethodInSortedMethodList(sel, mlist);
} else {
    // Linear search of unsorted method list
    if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
        return m;
}

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*这个函数是从排好序的搜索方法list:

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
if (list->isSmallList()) {
if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
} else {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
}
} else {
return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
}
}

接着我们来分析**findMethodInSortedMethodList**函数的代码:
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) 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)getName(probe);
        
        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)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

for循环中的count >>= 1的运算就是 就是取count的中间值,也就是平均值,即二分

 while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
 return &*probe;

这里就是主类的方法和分类的方法同名时,分类重写,也就是分类优先。
如果查找到的话,直接goto done,我们再来看下:

    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }

log_and_fill_cache 这里进行缓存填充,下次就不再进行二分查找,也就是在第一次查找的时候会进行慢速查找并插入到缓存中,下次就不再进行慢速查找。
如果没有查找到方法的话,会执行下面代码:

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

通过父类去查找,父类同样的先进行快速查找(汇编快速查找),如果没有找到再慢速查找,如果还是没找到,再拿到父类进行快速和慢速的查找,这就是递归的过程。
以上就是OC底层慢速查找流程。

动态方法决议

对象方法动态决议

首先我们来看下这张图:

3

从这张图我们都可以看出我们的方法未找到而报错,那么它的原理是什么,又做了什么操作呢,让我们一步一步往下分析。
首先我们在lookUpImpOrForward函数中第一行代码

const IMP forward_imp = (IMP)_objc_msgForward_impcache;

这里有一个默认的赋值imp。
而这行代码

 if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }

如果我们没有找到imp的话,会把默认的forward_imp给到imp,然后返回,通过一系列的分析(中间有汇编代码,我们这里不做介绍了),发现以下代码:

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

上面那张图就是这里打印出来的。
当我们的方法找不到的时候,会有一个过程,我们来分析一下。
首先我们断点一下,如图:


4

然后我们找到lookUpImpOrForward这个函数
断点调试,如图:

5

然后再进入这个行代码,如图:
6

然后我们看下imp对象是什么,如图:
7

这时候imp获取不到,这个时候会进入6500行这代码的流程,这个是单例,只会进入一次,这里的behavior通过查找是3,这里通过&运算后,就不会再进来了。
而这里会进入resolveMethod_locked这个函数,代码如下:

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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

如果我们的方法找不到的话,在这里我们还有一次机会进行拯救一下后返回imp,lookUpImpOrForwardTryCache这个函数会重新进行查找一次返回imp。那么是如何重新处理这个方法找不到的问题呢,我们接着分析。
我们来看下以下代码:

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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

这里判断cls(当前对象的类)如果不是元类,调起resolveInstanceMethod这个函数,如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // 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 = lookUpImpOrNilTryCache(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));
        }
    }
}

这里系统会自动发消息
BOOL (msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
*
如果在类里面resolve_sel有这个消息并且处理的话,就会调用IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);这行代码继续查找一遍,resolve_sel这个对象就是** resolveInstanceMethod这个,也就是说只要实现resolveInstanceMethod**这个方法,就可以进行容错处理,因为这是往cls(类)发送消息,也就是类方法。
然后我们在RoTeacher的m文件加入以下代码,如图:

8

我们来运行一下,如图:
9

然后我们在RoTeacher的m文件加入以下代码,如图:
10

进着我们再运行一下,看结果如何,如图所示:

11

我们的程序不再闪退了,可以正常运行了,这说明我们只要重新处理resolveInstanceMethod这个函数,就可以使程序更健壮一些,这就是对象方法的决议

类方法的动态决议

我们先看以下代码:

  // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }

这个是类方法动态决议的关键代码,我们接着分析,我们先断点调试,如图:


12

以下代码是类方法动态决议的代码:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(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 = lookUpImpOrNilTryCache(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 resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);这行代码的nonmeta是要写在元类里,而resolveClassMethod是一个类方法,类的类方法在元类里存在,所以resolveClassMethod这个方法写在RoTeacher类里。我们在RoTeacher中加入以下代码:

+ (BOOL)resolveClassMethod:(SEL)sel {
    
    NSLog(@"resolveClassMethod :%@-----%@", self, NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

然后运行,如图所示:


13

然后,我们再加入以下代码:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(sayHappy)) {
        IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("RoTeacher"), @selector(sayRo));
        Method method = class_getInstanceMethod(self, @selector(sayRo));
        const char *type = method_getTypeEncoding(method);
        return  class_addMethod(objc_getMetaClass("RoTeacher"), sel, sayNBImp, type);
    }
    NSLog(@"resolveClassMethod :%@-----%@", self, NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

接着我们运行看下结果:


14

这说明我们实现成功了,没有闪退,这说明类方法的动态决议,当然我们可以建一个NSObject分类统一处理,这里不再做介绍。

消息转发流程

快速转发流程

如果我们没有在方法动态决议这一层处理的话,程序依然会报错,那么我们还有没有机会处理呢,我们带着这个疑问往下继续探索。
我们在lookUpImpOrForward这个函数中没有找到转发流程的相关代码,那么该怎么办?
我们先来介绍下instrumentObjcMessageSends这个函数底层调用的预知打印,我们来试下,我们在objc的源码中搜索下instrumentObjcMessageSends这个函数,如下代码:

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

经过一系列的分析,我们有看到系统会写入日志文件,我们打开这个文件(/tmp/msgSend目录),找到了** forwardingTargetForSelector**这个方法,我们继续分析这个。

我们在官方文档下搜了下这个方法,如图:


14

这个方法的意思是返回一个对象成为当前没有被识别的消息的继承者,也就是重定向,这个是快速转发的执行者。
我们在RoTeacher的m的文件中加入以下代码:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%S - %@", __func__, NSStringFromSelector(@selector(aSelector)));
    return [super forwardingTargetForSelector:aSelector];
}

在头文件加入:

- (void)sayHello;

我们不在m文件中实现。
接着我们运行项目,如图:

15

我们通过打印日志可以看出我们的forwardingTargetForSelector接收到了,
我们先在RoPerson加入sayHello的实现,我们修改forwardingTargetForSelector

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s ------------- %@", __func__, NSStringFromSelector(aSelector));
    return [RoPerson alloc];
}

然后运行,如图:

16

我们看到这里没有闪退,说明成功转发到一个局部对象上了,这就是快速转发流程

慢速转发流程

如果RoPerson没有实现sayHello呢,在实际项目中,我们无法确定某一个方法一定会存在,这个时候我们该怎么处理呢,我们接着分析。
我们在官方文档搜索下** methodSignatureForSelector**这个方法,如图:


17

这个方法是返回当前方法的签名。
接着我们在RoTeacher加入以下代码:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s ------------- %@", __func__, NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}
18

从这张图中可以看出没进入慢速转发流程,methodSignatureForSelector没调用,在快速转发流程中,我们转发到了RoPerson,它会进行一系列的,快速查,慢速查找,动态方法决议流程,如果都没有实现,RoPerson的方法,但是RoPerson没有实现sayHello,所以没走到methodSignatureForSelector这个方法,我们注释掉快速转发流程,然后再运行,如图:

19

这样进入了慢速转发的流程,但是这里并没有解读成功,是因为这个方法需要另一个方法配合使用。
接着我们再修改RoTeacher的m文件,代码如下:

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
}

接着运行,如图:


20

这样没有闪退了,但是没做任何操作。
接着我们修改forwardInvocation代码如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    RoPerson *r = [RoPerson alloc];
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invoke];
    }
    else if([r respondsToSelector:anInvocation.selector] ) {
        [anInvocation invokeWithTarget:r];
    }
    else {
        NSLog(@"这里有闪退问题");
    }
    NSLog(@"%@----%@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
}

再次运行,如图:


21

这里一直存着方法。这就是慢速转发流程,相对于快速转发流程给我们更加自由灵活的处理。

结语

以上就是个人对objc_msgSend的汇编快速查找慢速查找动态决议快速转发慢速转发的一些见解,还有许多不完善的地方,请大家多指教。

补充 动态方法决议走两次的原因

我们先看张图,如下:

22

say666方法不存在,resolveInstanceMethod方法调用了两次,这是为什么,我们来分析下。
我们在resolveInstanceMethod打个断点调试,如图:
23

接着我们运行程序,我们在第二次打印之前,bt看下堆栈,如图:
24

我们在frame#12中看到_CF_forwarding_prep_0这个函数,这一步来自于CF调起了_CF_forwarding_prep_0,这里做了相关的操作,这又是为什么?我们接着往下看。
在这个过程中,我们做了消息转发的流程,我们的消息转发流程也是对程序做了容错处理。
通过搜索这个方法class_respondsToSelector_inst,发现里面又调了lookUpImpOrNilTryCache函数,最终调了lookUpImpOrForward函数,又再给一次机会进行补救,如果还是找不到方法,再进行动态方法决议,这也是为什么动态方法决议调起两次的原因。

我们再来看下面一张图:


25

从张图中,可以看出,第二次的say666的打印,是在快速和慢速转发流程后或者由它发起的调用,不是无限递归,也就是系统内部函数做了一个流程。

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

推荐阅读更多精彩内容