/本文主要分为3个阶段:快速查找(汇编部分)、慢速查找(Runtime部分)、动态决议及消息转发./

方法的本质

在搞清楚方法的本质之前,我们先来了解下什么是Runtime

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

上面这段话是官网上的,大概意思就是从编译到链接再到到运行时,Objective-C语言会尽可能多地推迟决策。只要有可能,它就会动态地执行操作。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译后的代码。也就是大家常说的Rumtime. runtime在我们的项目中很常见,使用的方式有很多种例如:

  • [person sayHello]
  • [NSObject isKindOfClass]
  • class_getInstaceSize
他们之间对应的关系如下图所示

Compiler就是我们常说的编译器,即LLVM。例如在项目中常见的alloc方法,编译成C或C++之后就变成了objc_alloc,Runime System Library 就是我们的系统底层库。

接下来我们通过clang编译一下源码

//main.m中person的两个方法    
Person *person = [Person alloc];
[person sayHello];
[person say666];

//clang编译后的底层代码
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("say666"));

我们看到编译完之后所有的方法都是通过调用objc_msgSend来进行发送消息,接下来我们 看看它到底都干了什么?

我们接下来的案例都是基于arm64架构和objc4-781源码进行分析

快速查找

我们在源码工程中搜索objc_msgSend,c++文件中没有找到对应的实现,在.s汇编中看到入口 ENTRY _objc_msgSend

接着看看_objc_msgSend汇编源码实现

    //消息发送汇编入口
    ENTRY _objc_msgSend
    //无窗口
    UNWIND _objc_msgSend, NoFrame
    //判断是否为空,p0是_objc_msgSend的第一个参数即消息的接受者receiver
    cmp p0, #0          // nil check and tagged pointer check
    //是否支持taggedpointers小对象类型
#if SUPPORT_TAGGED_POINTERS
    //如果支持taggedpointer,跳转 LNilOrTagged流程
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    //如果p0 等于 0 时,直接返回 空
    b.eq    LReturnZero
#endif
    //到这里p0(也就是receiver)肯定不为空
    //根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器p13 = isa
    ldr p13, [x0]       // p13 = isa
    //在通过 p16 = isa(即p13) & ISA_MASK,得到class信息
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    //拿到isa和class信息,开始缓存查找流程,也就是所谓的sel-imp快速查找流程
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // 等于空,返回空
    b.eq    LReturnZero     // nil check
    // 不然通过一系列操作拿到isa然后跳转LGetIsaDone
    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
// 返回空
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    // 结束
    END_ENTRY _objc_msgSend

以上主要从当前的消息接收者receiver中查找到receiver对应的isa指针进而找到对应的类

接下来我们再看下通过isa找到class的GetClassFromIsa_p16实现

.macro GetClassFromIsa_p16 /* src */
.....
//这里64位相关
#elif __LP64__
    // 64-bit packed isa
    // p16 = class = isa & ISA_MASK(通过isa.h中的ISA_MASK掩码进行位运算 & 即获取isa中的shiftcls信息)
    and p16, $0, #ISA_MASK
.....
#endif
.endmacro

最终拿到isaclass,开始重点缓存查找流程,即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.x16(isa)偏移CACHE(cache=2*8即16)拿到cache的地址存在p11中
    //p11 = mask|buckets也就是从x16(即isa)中平移16字节,取出cache存入寄存器p11中(class 结构体中isa占8字节,superclass占8字节,后面才是cache的地址,所以需要平移16,cache中包含高16位mask和低48位buchets)
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets
//如果是64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // p11(cache)&上0x0000ffffffffffff就是把高的16位mask抹零得到buckets,放入寄存器p10中
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    // 这里把p11(cache)右移48位(buckets)也就是把buckets抹零拿到mask,在通过mask&p1(msg_send的第二个参数即sel)得到哈希下标index,然后存入p12(这里的哈希算法和cache insert是一样的都是通过mask&sel)
    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

    //PTRSHIFT=3
    //p12是哈希下标,p10是buckets的首地址
    //((_cmd & mask) << (1+PTRSHIFT)),就是把p12(hash值下标)左移4位(也就是*16),得到实际偏移大小,通过buckets(首地址)+偏移的地址得到index所对应的bucket,buckets的结构为{sel,imp}
    //获取bucket存入p12寄存器
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    // 从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    // 比较 sel 与 p1(传入的参数cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    // 如果不相等,即没有找到,请跳转至 2f
    b.ne    2f          //     scan more
    // 如果相等 即cacheHit 缓存命中,直接返回imp
    CacheHit $0         // call or return imp

2:  // not hit: p12 = not-hit bucket
    // 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
    // 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素,)
    cmp p12, p10        // wrap if bucket == buckets
    // 如果等于,则跳转至第3步
    b.eq    3f
    // 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    // 继续跳转至第1步,继续对比 sel 与 cmd
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // 指针指向buckets到最后一个元素
    // p11(cache)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找,找到最后一个bucket 存入p12
    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.
    现在指向的是最后一个bucket(即x12),拿出 {imp, sel}存入p17和p9
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    // 比较 sel 与 p1(传入的参数cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    // 如果不相等,即goto到第二步
    b.ne    2f          //     scan more
    // 如果相等 即命中,直接返回imp
    CacheHit $0         // call or return imp

2:  // not hit: p12 = not-hit bucket
    // 如果一直找不到,且sel=0(即为空)则CheckMiss
    CheckMiss $0            // miss if bucket->sel == 0
    // 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到
    cmp p12, p10        // wrap if bucket == buckets
    // 如果相等,跳goto第3步(即 JumpMiss $0)
    b.eq    3f
    // 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到前一个bucket元素,imp-sel分别存入p17-p9,即向前查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    // 跳转至第1步,继续对比 sel 与 cmd
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    // 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached
    JumpMiss $0

.endmacro

复制代码

以下是CacheHitCheckMissJumpMiss汇编实现

// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
//  如果是 NORMAL 执行
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
//返回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, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
    //判断bucket->sel == 0
    // miss if bucket->sel == 0
    // 如果为GETIMP ,则跳转至 LGetImpMiss
.if $0 == GETIMP
    cbz p9, LGetImpMiss
    //  如果为NORMAL ,则跳转至 __objc_msgSend_uncached
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
    // 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

同 CheckMiss
.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
 // 如果是NORMAL执行 __objc_msgSend_uncached
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
    如果为LOOKUP ,执行__objc_msgLookup_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

我们来梳理一下上面的执行过程

  • 通过isa地址偏移16字节拿到buckets

    • 在class结构结构体中,第一个是isa占8个字节,第二个是superclass占8个字节,第三个是cache,所以偏移16字节,存入寄存器p11
  • 从cache中取出buckets和mask(cache中mask占高16位,buckets占低48位)。

    • cache&0x0000ffffffffffff即抹掉高16位得到buckets,存入寄存器p10
    • mask通过cache右移48位,得到高16位即mask,存入寄存器p11
  • 将objc_msgSend的参数p1(即第二个参数_cmd)& msak,通过哈希算法,得到需要查找存储sel-imp的bucket下标index,即p12 = index = _cmd & mask,这里cache存储的时候也是通过这种方式生成的hash值,所以读取也需要通过同样的方式读取。

  • 根据所得的哈希下标index 和 buckets首地址,取出哈希下标对应的bucket

    • PTRSHIFT等于3,左移4位(即2^4 = 16字节)为了计算一个bucket实际占用的大小,结构体bucket_t中sel占8字节,imp占8字节,所以是16

    • 再根据计算的哈希下标index 乘以 单个bucket占用的内存大小(16),实际内存中实际对应index需要偏移量

    • 通过首地址 + 实际偏移量,获取哈希下标index对应的bucket

  • 根据获取的bucket,取出其中的imp存入p17,取出sel存入p9

  • 循环递归查找

    • 比较获取的bucket中sel 与 objc_msgSend的第二个参数的_cmd(即p1)是否相等

    • 如果相等,则直接跳转至CacheHit,即缓存命中,返回imp

    • 如果不相等,有以下两种情况

      • 如果一直都找不到,直接跳转至CheckMiss,因为$0是normal,会跳转至__objc_msgSend_uncached,即进入慢速查找流程

      • 如果根据index获取的bucket 等于 buckets的第一个元素,将当前bucket设置为buckets的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到buckers的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环)

      • 如果当前bucket不等于buckets的第一个元素,则继续向前查找,进入第一次递归循环

  • 第二层循环递归

    • 流程和上面的第一层基本一样,有个区别就是如果再次循环到第一个bucket就直接JumpMiss。这个时候$0为normal,就会走到__objc_msgSend_uncached这个方法,进入慢速查找
下面是流程图

以上就是整个快速查找流程,我们看到如果没有在cache中找到imp,就会执行__objc_msgSend_uncached,也就是进入慢速查找

慢速查找

上面的快速查找过程中如果没有找到缓存,不管是CheckMiss还是JumpMiss,最终我们看到都走到了__objc_msgSend_uncached这个汇编函数

我们接着往下看,objc-msg-arm64.s文件中查找__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

通过上面的汇编代码我们继续往下看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,在文件中搜_lookUpImpOrForward,发现没有这个汇编函数了

我们来通过汇编调试一下看看函数调用的流程,首先我们在main函数调用方法中打个断点,如下图

这里我们看到底层调用的objc_msgSend,我们开启汇编调试【Debug -- Debug worlflow -- 勾选Always show Disassembly】,运行程序

汇编中objc_msgSend加一个断点,执行断住,按住control + stepinto,进入objc_msgSend的汇编
在_objc_msgSend_uncached加一个断点,执行断住,按住control + stepinto,进入汇编

从上可以看出最后走到的就是lookUpImpOrForward,此时并不是汇编实现,而是一个.mm的C++文件。根据汇编部分的提示,全局续搜索lookUpImpOrForward,最后在objc-runtime-new.mm文件中找到了源码实现,这是一个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();

     // 快速查找,如果找到则直接返回imp,防止多线程操作时,刚好调用函数,此时缓存进来了
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    //加锁,目的是保证读取的线程安全
    runtimeLock.lock();

    //检查是否是一个已经加载的类
    checkIsKnownClass(cls);
    //判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    //判断类是否初始化,如果没有,需要先初始化
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);

    }

    runtimeLock.assertLocked();
    curClass = cls;
    //核心代码,查找类的缓存
    // unreasonableClassCount -- 表示类的迭代的上限
    for (unsigned attempts = unreasonableClassCount();;) {
        //---当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中

        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //当前类 = 当前类的父类,并判断父类是否为nil,即找到了NSObject了没有父类了
        if (slowpath((curClass = curClass->superclass) == nil)) {
           //--未找到方法实现,方法解析器也不行,使用转发
            imp = forward_imp;
            break;
        }

         // 如果父类链中存在循环,则停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // --父类缓存
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
           // 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器
            break;
        }
        if (fastpath(imp)) {
             //如果在父类中,找到了此方法,将其存储到cache中
            goto done;
        }
    }

    //没有找到方法实现,尝试一次方法解析

    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;
    }
    //返回imp
    return imp;
}

上面的方法主要以下几个步骤:

  • 从cache中在查找一次缓存,如果找到直接返回imp,如果没有找到,进入下一步
  • 判断class
    • 是否是已知类,不是就报错
    • 类是否实现,如果没有,则需要先实现,确定其父类链,此时实例化的目的是为了确定父类链、ro、以及rw等等等,方法后续数据的读取以及查找的循环
    • 是否初始化,如果没有,则初始化
  • 循环按照类继承链 或者 元类继承链的顺序查找
    • 当前class的方法列表中使用二分查找算法查找方法,如果找到,则进入cache写入流程,并返回imp,如果没有找到,则返回nil

    • 当前class被赋值为父类,如果父类等于nil,则imp = 消息转发forward_imp,并终止递归,进入下一步

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

    • 父类缓存中查找方法

      • 如果未找到,则直接返回nil,继续循环查找
      • 如果找到,则直接返回imp,执行cache写入流程
  • 判断是否执行过动态方法解析
    • 如果没有,执行动态方法解析

    • 如果执行过一次动态方法解析,则走到消息转发流程

流程图如下:

以上就是整个慢速查找的流程,接下来我们看下二分查找法的具体源码getMethodNoSuper_nolock和过程

核心代码在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;//key等于sayhello
    uint32_t count;
    //base相当于low,count是max,probe是middle,这就是二分
    for (count = list->count; count != 0; count >>= 1) {
     //从首地址+下标 --> 移动到中间位置(count >> 1 左移1位即 count/2 = 4)
        probe = base + (count >> 1);

        uintptr_t probeValue = (uintptr_t)probe->name;
        //如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置
        if (keyValue == probeValue) {
            // -- while 向前平移 -- 分类中如果有重名方法,往前查找,找到不相等的停止
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                //如果是两个分类,就看谁先进行加载
                probe--;
            }
            return (method_t *)probe;
        }
        //如果keyValue 大于 probeValue,就往probe即中间位置的右边查找
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }

    return nil;
}

大概查找流程就是从第一次开始查找,每次都取中间位置,与想查找的key的value值作比较,如果相等,继续往前找,找到不相等的停止。这里主要考虑分类重名方法,然后将查询到的位置的方法实现返回,如果不相等,则需要继续二分查找,如果循环至count = 0还是没有找到,则直接返回nil

流程图如下:

父类中通过cache_getImp进行查找,cache_getImp是汇编_cache_getImp

STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp
此时我们看到_cache_getImp还是调用CacheLookup,0为GETIMP,如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,如果在父类缓存中没有找到方法实现,则跳转至CheckMiss或者JumpMiss,通过判断0为GETIMP,如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,如果在父类缓存中没有找到方法实现,则跳转至CheckMiss 或者 JumpMiss,通过判断0为GETIMP,如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,如果在父类缓存中没有找到方法实现,则跳转至CheckMiss或者JumpMiss,通过判断0 跳转至LGetImpMiss,直接返回nil则直接返回imp大概流程如下:

如果在快速查找和慢速查找中都没有找到的情况下,就会进入接下来的下一个环节

动态决议及转发

当我们在快速查找和慢速查找中都没有找到imp的情况下,系统会建议我们做一些补救措施

  • 动态方法决议:快速查找和慢速查找都没有找到胡调用一次
  • 消息转发:如果动态方法决议中仍没有找到,进入消息转发流程

如果这两步我们都没有做处理,这个时候系统就会抛出异常,接下来我们测试一下

我们给Person类添加一个方法接口sayNB,.m中没有实现sayNB这个方法。main函数中调用sayNB

@interface Person : NSObject
- (void)sayHello;
- (void)say666;
- (void)sayNB;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"%s", __func__);
}

- (void)say666 {
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...

        Person *person = [Person alloc];
        [person sayHello];
        [person say666];
        [person sayNB];
    }
    return 0;
}
运行程序,这个时候看到熟悉的崩溃错误日志

我们来看看系统是如果打印的这个日志,在前面我们了解到如果慢速查找没有找到imp,会找到forward_imp,即_objc_msgForward_impcache,我们看看他的源码

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    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_forward_handler看到以下代码

// 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,这个里面打印了我们熟悉的没有实现函数,运行程序,崩溃时报的错误提示xxx unrecognized selector sent to instance xxx

接下来我们看下如果系统是怎么样给我补救的机会,我们应该怎么补救

动态方法决议

在慢速查找流程未找到方法实现时,首先会尝试一次动态方法决议,其源码实现如下:

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 {//如果cls是元类,调用类的解析方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        //类方法在元类中是对象方法,所以还是需要查询元类中对象方法的动态方法决议
        if (!lookUpIm***il(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);
}

大概流程就是

  • 如果cls是类,执行实例方法的动态方法决议resolveInstanceMethod
  • 如果cls是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程

接下来我们看下实例方法resolveInstanceMethod实现

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

    // look的是 resolveInstanceMethod --相当于是发送消息前的容错处理
    if (!lookUpIm***il(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); //发送resolve_sel消息

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //查找sayNB
    IMP imp = lookUpIm***il(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));
        }
    }
}

上面的流程大概就是

  • 在发送resolveInstanceMethod消息前,需要查找cls类中是否有该方法的实现,即通过lookUpIm***il方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
  • 如果没有,则直接返回
  • 如果有,则发送resolveInstanceMethod消息

接下来我们来重写一下Person的resolveInstanceMethod方法

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(self, @selector(sayError));
        //获取sayError的实例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayError));
        //获取sayError的丰富签名
        const char *type = method_getTypeEncoding(sayMethod);
        //将sel的实现指向sayError
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
判断一下如果sel等于sayNB,获取sayError的imp和方法签名,最后把sayNB的sel和sayError的imp进行关联,这个就会调用sayError方法,我们再次运行,果然走到sayError方法中
我们如果重写resolveInstanceMethod不关联sel和imp会怎么样呢?

/打印结果,你会发现走了两次sayNB 来了,为什么呢,这个后面细说/

类方法的resolveClassMethod和resolveInstanceMethod思路基本一样,我们添加一个类方法+ (void)sayClass,重写+ (BOOL)resolveClassMethod:(SEL)sel

+ (BOOL)resolveClassMethod:(SEL)sel{
     if (sel == @selector(sayClass)) {
            NSLog(@"resolveClassMethod--%@ 来了", NSStringFromSelector(sel));
            IMP imp = class_getMethodImplementation(self, @selector(sayError));
            Method sayMethod  = class_getInstanceMethod(self, @selector(sayError));
            const char *type = method_getTypeEncoding(sayMethod);
            return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
        }
   return [super resolveClassMethod:sel];
}

打印结果

这里需要注意传入的cls不再是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,类方法在元类中是实例方法。

消息转发

在慢速查找的流程中,我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发,但是源码也没有发现消息转发的相关源码,接下来我们可以通过以下两种方式来了解程序崩溃前都走了哪些方法

instrumentObjcMessageSends方式打印发送消息的日志

通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现,所以,在main中调用 instrumentObjcMessageSends打印方法调用的日志信息

准备工作

1、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES

2、在main中通过extern 声明instrumentObjcMessageSends方法,运行程序,并前往/tmp/msgSends 目录,发现有msgSends开头的日志文件,打开发现在崩溃前,执行了以下方法

resolveInstanceMethod方法执行了两次,forwardingTargetForSelector方法执行两次。methodSignatureForSelector + resolveInstanceMethod都执行了两次

hopper/IDA反编译(以下是Hopper为例)
我们在崩溃的日志中看到

___forwarding___ 来自CoreFoundation,我们通过image list读取下镜像文件,然后搜索CoreFoundation,查看其可执行文件的路径

通过文件路径,找到CoreFoundation的可执行文件

下面是 forwarding_prep_0_ 伪代码

快速转发

这里看到调用了 forwarding,下面是____forwarding__伪代码

如果没有实现forwardingTargetForSelector跳转至loc_64c47,else能响应但返回值为空也跳转至loc_64c47

慢速转发
如果methodSignatureForSelector没有实现跳转至loc_64fb7,执行CFLog,最后跳转loc_6501c
如果实现了但返回空跳转至loc_6501c
最终都执行doesNotRecognizeSelector方法,如果methodSignatureForSelector能响应且不为空执行_forwardStackInvocation
如果返回为空跳转loc_64df9
forwardInvocation方法中对invocation进行处理
至此,消息转发的整个流程如下:

消息转发的处理主要分为两部分:

  • 【快速转发】当慢速查找,以及动态方法决议均没有找到实现时,进行消息转发,首先是进行快速消息转发,即走到forwardingTargetForSelector方法

如果返回消息接收者,在消息接收者中还是没有找到,则进入另一个方法的查找流程

如果返回nil,则进入慢速消息转发

  • 【慢速转发】执行到methodSignatureForSelector方法

如果返回的方法签名为nil,则直接崩溃报错

如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也不会报错。

关于resolveInstanceMethod执行两次的分析
前文提到为什么resolveInstanceMethod执行两次 我们发现在resolveInstanceMethod方法中不添加Method才会走两次,正常添加了只走一次,说明是在resolveInstanceMethod动态决议之后调用的。

在resolveInstanceMethod打个断点,通过resolveInstanceMethod时的堆栈信息,可以发现是被methodSignatureForSelector方法中调起的

通过反汇编在CoreFoundation的[NSObject methodSignatureForSelector:]可以看到整个的实现。
methodSignatureForSelector调用了class_getInstanceMethod
我们通过断点调试打印堆栈发现确实是这里
经过上面的论证,我们了解到其实在慢速消息转发流程中,在methodSignatureForSelector 和 forwardInvocation方法之间还有一次动态方法决议,即苹果再次给的一个机会,如下图所示

都这里整个消息查找、转发的流程就走完了主要经历了快速查找——>慢速查找——>动态决议——>快速转发——>慢速转发这几个阶段

作者:wpina
链接:https://juejin.im/post/6884097012678328328