iOS底层之objc_msgSend慢速查找流程

iOS底层之objc_msgSend快速查找流程里分析里调用方法的本质,就是消息发送,查找类的方法缓存,那么如果经历CacheLookup后没找到缓存,即快速查找流程找不到,则会开始慢速查找,从methodList查找,这一篇文章我们来分析方法的查找流程。

CacheLookup快速查找流程中,当没有找到方法imp缓存,无论是走到CheckMiss还是JumpMiss,最终都会走到__objc_msgSend_uncached汇编函数。其定义是:

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band r10 is the searched class

    // r10 is already the class to search
    MethodTableLookup NORMAL    // r11 = IMP
    jmp *%r11           // goto *imp

    END_ENTRY __objc_msgSend_uncached

可以看到关键代码MethodTableLookup,会查找方法表。
其定义是:

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

其中关键代码是_lookUpImpOrForward。为什么?
通过调用一个实例方法[person sayHello],并打断点。打开汇编调试查看:


可以看到汇编停在了[person sayHello]调用之前,下一步开始发送消息调用sayHello。打断点objc_msgSend,按住control+点击step into进入内部调用。

_objc_msgSend_uncached之上完成了类信息获取,方法快速查找流程,没找到缓存时来到了_objc_msgSend_uncached,断点跳到这一句,继续step into进入内部。

可以看到来到了lookUpImpOrForward函数,显示这个函数在objc-runtime-new.mm文件的6099行。
也就验证了上面我们说的_objc_msgSend_uncached之后会来到_lookUpImpOrForward

通过汇编源码查找C/C++源码的技巧
例如_lookUpImpOrForward,
汇编中查找 C/C++方法时,需要将汇编调用的方法_lookUpImpOrForward去掉一个下划线

从_objc_msgSend_uncached过来时behavior 是LOOKUP_INITIALIZE | LOOKUP_RESOLVER,该枚举定义是

/* method lookup */
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_CACHE = 4,
    LOOKUP_NIL = 8,
};

搜索找到lookUpImpOrForward的定义,主要的步骤分析我都写在注释里了。

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //_objc_msgForward_impcache获取消息转发的函数地址,用于方法未找到并且动态方法决议未处理时调用。如果消息转发也不成功,则程序会抛出方法未识别的错误
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    //当behavior&LOOKUP_CACHE==1时,查找下缓存。为了防止多线程操作时,调用函数时,此时另一线程缓存进来了,可以快速查找一遍缓存,因为下面的慢速查找很耗时
    //1.当从MethodTableLookup过来时,behavior=3,3&4=0为假,往下执行
    //2.动态方法决议回来第一次时'sel=resolveInstanceMethod',此时behavior=12 12&4=4为真,这时还未缓存resolveInstanceMethod,imp为空,往下执行)
    //3. 动态方法决议回来第二次时'sel=say666',此时behavior=12, 12&4=4为真,无论动态方法决议流程+resolveInstanceMethod里是否正确添加了imp,此时缓存中依旧为空,往下执行
    //4.动态方法决议处理后,会从resolveMethod_locked的lookUpImpOrForward重新进来一次,此时behavior=5,5&4=4为真,此时找到的imp为动态方法决议第二次进来时缓存的say666的方法地址,跳转done_nolock(不缓存)
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    //在isRealized和isInitialized检查期间保持锁,防止对并发实现的竞争
    //runtimeLock在方法搜索过程中被保持
    //方法查找+缓存填充相对于方法添加而言是原子的
    //保证方法查找过程中method-lookup + cache-fill中方法添加的原子性。
    // 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();

    // TODO: this check is quite costly during process startup.
    //检查是否是已知类(已经加载的类)
    //确保这个类是通过objc_duplicateClass, objc_initializeClassPair或objc_allocateClassPair合法注册的,或者内置到二进制文件中的,而不是创建一个看起来像类而实际并不是的二进制的blob,做CFI攻击
    checkIsKnownClass(cls);
    
    //小概率情况下,当前类未实现时
    //如果没有,需要先实现,目的是为了能确定父类继承链和元类继承链,后面查找递归,当前类没找到,则需要从继承链查找
    //内部有双向链表会更新子类和父类
    //还会把类的相关属性协议方法贴到rw.ext()中
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
    //判断如果类没有初始化,则会递归父类继承链执行初始化所有的类
    //没实现时会callInitialize对类发送消息调用initialize方法
    //所以侧面说明了initialize和load方法一样,不需要主动调用,底层实现时帮你调用
    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().
    
    //重点!!!!从这里对imp赋值
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        //查找curClass方法列表,如果有,就不需要去找curClass的父类了
        //1.当动态方法决议回来第一次时'sel=resolveInstanceMethod',如果curClass(或者递归的父类链)方法列表实现了,则赋值imp,执行done
        //2.当动态方法决议回来第二次时'sel=say666',此时curClass的方法列表可以找到imp,跳到done
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        //如果找到了,则跳转到done(缓存方法、解锁)
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //本类没找到,则将curClass赋值为当前类的父类
        //如果父类是nil,也就是继承链走完了,那么会imp = forward_imp走消息转发,跳出循环
        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.
        //查找当前类的缓存,注意,上面已经将curClass父类赋值给当前类,所以是查找父类的缓存,并不是递归当前类了
        //查找过程是`cache_getImp` - `lookup` - `lookUpImpOrForward`,也是通过`cache_getImp`查找缓存,不过参数是`GETIMP`,跟上面NORMAL区别是找不到缓存时,不是走`_objc_msgSend_uncached`,而是走`LGetImpMiss`,最后会返回nil,避免了导致死循环。
        imp = cache_getImp(curClass, sel);
        //上面当父类链的父类为nil时,imp = forward_imp进行赋值,所以走下面这一步说明了父类链已经走完都没找到。这时跳出循环,首先会调用该类的方法解析器
        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;
        }
        //如果找到了父类imp,则跳转done(缓存方法、解锁),缓存在curClass(当次循环中curClasss是入参类则缓存到入参类,是某级父类就缓存到某级父类中)
        //1.当动态方法决议回来第一次时'sel=resolveInstanceMethod',这时候从cache找的imp是空的,不走进这里。
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
        //当前父类没有找到,由于for循环没有跳出的条件判断,会一直死循环查找父类继承链,直到break
    }

    // No implementation found. Try method resolver once.
    //这一步代表了流程只走一次下面这段代码(也就是只会走一次动态方法决议),因为 behavior ^= LOOKUP_RESOLVER改变了behavior,与条件behavior & LOOKUP_RESOLVER = 1不能同时满足
    //LOOKUP_RESOLVER =2,behavior & LOOKUP_RESOLVER也就是3&2=2,执行条件语句代码块
    //1.从CachLookup后进来behavior=3,3&2为真
    //2.从动态方法决议回来第一次:12,此时sel为resolveInstanceMethod,12&2为假
    //3.从动态方法决议回来第二次:12,此时sel为say666,12&2为假
    //也就是保证了动态方法决议只会被执行一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //behavior = 3^2 = 1
        behavior ^= LOOKUP_RESOLVER;
        //没有找到imp,则开始走本类的动态方法决议
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    //缓存方法
    //当从入参类或者父类链中找到imp,则缓存sel和imp到当前循环中的curClass中。1.当从_objc_msgSend_uncached通过NORMAL进来直接在入参类/父类继承链的方法列表中查找到imp时,此时sel和imp均为say666 1.如果是动态方法决议第一次进来,sel='resolveInstanceMethod',imp为resolveInstanceMethod的实现地址 2.如果是动态方法决议第二次进来,此时sel='say666',imp为动态方法决议添加的say666的方法地址
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    //解锁
    runtimeLock.unlock();
 done_nolock:
    //当动态方法决议处理完成,从resolveMethod_locked的lookUpImpOrForward重新进来,此时behavior=5,5&8=0为假,执行最后的return imp。此时的imp是动态方法决议第二次进来时缓存的say666的方法地址。就这样如果动态方法决议阶段+resolveInstanceMethod里正确添加了sel的imp,那在这一步就找到了imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    //当动态方法决议整个流程都没找到imp时,那么此时的imp就是forward_imp,也就是_objc_msgForward_impcache,回到MethodTableLookup,把imp的值给x17,接着TailCallFunctionPointer x17,也就是调用默认的转发处理_objc_msgForward_impcache这个方法,进入消息转发流程。如果依然未处理,则抛出消息未识别的错误,终止进程。
    return imp;
}

主要步骤是:

  • 【第一步】由于多线程可能导致方法缓存在另外线程添加,为了避免下面慢速查找继承链方法列表需要消耗大量时间,所以再次从cache缓存中进行查找,即快速查找,找到则直接返回imp,否则,则进入【第二步】

  • 【第二步】判断cls是不是已知类

    • 如果不是,则报错

    • 类是否已实现,如果没有,则需要先实现,确定其父类链或元类继承链,此时实例化的目的是为了确定父类链、ro、以及rw等,方法后续数据的读取以及查找需要

    • 是否已初始化initialize,如果没有,则初始化

  • 【第三步】for循环,按照类继承链 或者 元类继承链的顺序查找

    • 当前类的方法列表中使用二分查找算法查找方法,如果找到,则将方法写入cache(在iOS底层之cache_t探究中分析了写入过程),并返回imp,如果没有找到,则返回nil

    • 当前cls被赋值为父类,如果父类等于nil,则imp = forward_imp,并跳出循环,进入【第四步】

    • 如果父类链中存在循环,则报错,终止循环

    • 从父类缓存中查找方法

      • 如果未找到,进入下一次循环,Method meth = getMethodNoSuper_nolock(curClass, sel);,curClass变为其父类,查找父类方法列表

      • 如果找到,则直接返回imp,执行cache写入流程

  • 【第四步】判断是否执行过动态方法决议

    • 如果没有,执行动态方法决议

其中部分函数的源码

  1. 二分法查找方法列表的imp
  • getMethodNoSuper_nolock查找当前类的方法列表
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    //查找data里的methods() 方法列表
    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;
}
  • 重点!!上面的函数,重点在于findMethodInSortedMethodList(sel, mlist);以二分法查找
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    //list为data()中的方法列表
    ASSERT(list);

    const method_t * const first = &list->first;
    //取base为第一个元素
    const method_t *base = first;
    const method_t *probe;
    //key为传进来的cmd,也就是我们调用的sayHello
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    //count赋值为列表元素个数,每一次循环都将count/2
    for (count = list->count; count != 0; count >>= 1) {
        //base为二分法区间的最小元素。将base平移count/2,也就是平移到列表中间的位置,probe为中间的元素
        probe = base + (count >> 1);
        //获取probe存储的名字,也就是sel方法名
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        //如果查找的cmd等于sel
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            //由于分类的重写的方法加载到内存中是会插在类的同名方法前面,所以这里需要循环向前一个查找sel,如果有,则返回分类的sel。
            //如果存在多个分类重写,则看哪个分类先加载
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        //如果cmd大于sel,则需要往后面查找,赋值最小值base为中间值probe+1,如果cmd小于sel,则不走进来
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
        //没找到一致的sel,则进入下一次循环,再取base和count的中间值
    }
    //如果循环到count==0,也没找到,则返回nil
    return nil;
}

其执行流程图如下


二分法查找方法列表流程
  1. 查找父类imp源码:
    _cache_getImp入口-> CacheLookup (GETIMP)
STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp

开始CacheLookup (GETIMP),如果命中则CacheHit,未命中则CheckMiss 或者JumpMiss ->

.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 / JumpMiss -> LGetImpMiss,则执行LGetImpMiss

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

.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

其流程图:


父类缓存查找流程图.png
  1. 如果父类链没找到imp,则进入动态方法决议resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    //lookUpImpOrForward没找到imp,再给你一次机会,去处理
    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);
        //再查找一次methodList,如果还是nil,会查找一次当前类的动态方法决议
        if (!lookUpImpOrNil(inst, sel, cls)) {
            //走当前类的动态方法决议
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //上面给你机会去处理,现在重新找一次lookUpImpOrForward,此时behavior | LOOKUP_CACHE = 1|4 = 5
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
* 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:);
    //如果resolveInstanceMethod方法未实现,则直接退出,这一步会触发一次查找lookUpImpOrNil-> lookUpImpOrForward,这时的behavior为0|LOOKUP_CACHE | LOOKUP_NIL=4|8=12
    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));
        }
    }
}

看看lookUpImpOrNil的实现

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

先调用了resolveInstanceMethod去看看resolver中帮没帮实现,如果帮实现了,在走lookUpImpOrNil过程中就会把方法缓存起来。然后会回到resolveMethod_locked方法中调用lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE),这时可以从缓存找到imp,进入done_nolock, 返回方法的imp;如果整个慢速查找流程(包括动态方法决议)都没有找到,就会返回存着forward_impimp。然后就又会回到MethodTableLookup,把imp的值给x17,接着TailCallFunctionPointer x17,调用forward_imp也就是_objc_msgForward_impcache这个方法,进去消息转发流程。

元类的动态方法决议流程和上面类实例的流程差不多,这里不再做分析。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));
        }
    }
}

为什么查找元类方法时在执行完_class_resolveClassMethod之后会再次执行一次_class_resolveInstanceMethod?

比如有一个类Person,我们查找 Person 的一个类方法,如果没找到,会继续找他的第一个元类,再找不到,会继续找根元类 ,最终会找到 NSObject。
Person(类方法) 找——> 元类(实例方法) 找——> 根元类(实例方法) 找——> NSObject(实例方法)
实例方法存在类对象里面,类方法存在元类里面。
当遍历元类继承链后都没找到时,就找一遍NSObject的实例方法列表。


isa走向.png
  1. 下面来看看_objc_msgForward_impcache做了什么__objc_msgForward_impcache
STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    //进入__objc_msgForward
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward
    //把存有__objc_forward_handler的页存入X17
    adrp    x17, __objc_forward_handler@PAGE
    //把x17向后偏移了__objc_forward_handler@PAGEOFF,然后把X17中地址的值存入p17,
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

TailCallFunctionPointer是个方法指针,调用了上面的__objc_forward_handler

.macro TailCallFunctionPointer
    // $0 = function pointer value
    br  $0
.endmacro

由于查找__objc_forward_handler未找到汇编定义,去掉一个_查找C/C++源码:

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

可以看到是由objc_defaultForwardHandler定义的。而这个函数里面,我们看到了熟悉的方法选择器未找到的打印信息。从打印信息的组成class_isMetaClass(object_getClass(self)) ? '+' : '-',可以看到,系统内部并没有区分方法是实例方法还是类方法,而是通过是否是元类,来区分打印方法类型。
其流程图:

然而在报错之前,还会对消息进行转发和方法重签名调用,再次挽救。加上动态方法决议,共有3次挽救机会。

消息转发机制

这个流程留待下一节分析。

总结

查找实例方法,会在中查找,慢速查找的继承链是:类->父类->根类->nil

同理,查找类方法,则在元类中查找,其慢速查找的链是:元类->根元类->根类->nil

如果objc_msgSend快速查找缓存,慢速查找当前类方法列表、父类的缓存和方法列表都没有找到imp方法实现,则尝试动态方法决议
如果动态方法决议仍然没有找到,则进行消息转发
如果消息转发还没有处理,则进去方法签名处理调用
如果方法签名处理调用还未处理,则程序会抛出错误。

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