Runtime学习-方法以及消息机制

1、类中方法的存储

cache_t中的方法存储

cache_t cache方法缓存中,方法的存储是以SELIMP的形式。

cache_t方法存储.png

class_data_bits_t中方法存储

在类objc_class的学习中,明白了实例对象的方法是存放在类的class_data_bits_t bits;中。取方法的时候,使用class_data_bits_t中的class_rw_t* data()。在这个地方,如果是在编译期,或者说在系统调用运行时的realizeClass方法之前,data()class_ro_t结构体的形式,保存了类中的方法列表、属性列表、成员变量列表等;在系统运行了runtime的realizeClass方法后,则会生成class_rw_t结构体,并更新class_rw_t* data()的地址,换成class_rw_t结构体的地址。

class_rw_tmethod_array_t-->method_list_t-->method_t.
class_ro_tmethod_list_t-->method_t.

method_rw_ro_t.png

方法以 method_t 结构保存,iOS14 以上该结构发生巨大变化。
在 64 位的系统上会占用 24 字节,name、types、imp 分别占用 64 bit 大小,与之前一样。

  • 取值时,在地址后取三个64位的数据,就能得到方法的name、type、imp
    但是 struct small 占用 12 字节,name、types、imp 分别占用 32 bit 大小。
  • 这种情况下,name、types、imp存储的是地址的偏移量。当前地址 + 存储的偏移量才是真正的存储地址。

name、types、imp 分别指向方法的 名称、参数数据、函数指针,苹果考虑到镜像中的方法都是固定的,不会跑到其他镜像中去。其实不需要 64 位寻址的指针,只需要 32 位即可 (多余 32 位寻址,可执行文件在内存中要超过 4G)。small 结构里面的数据,都是相对地址偏移,不是内存中的具体位置。如果要还原,需要进行计算。
该部分主要是以参考学习--iOS 恢复调用栈(适配iOS14),如需详细了解,请移步该文章。

method_t结构体.png

2、消息机制

(1)、方法调用的编译过程

当我们在程序中调用方法时,格式为[obj method]。经过编译过程,会转换为objc_msgSend(调用父类方法,会转换为objc_msgSendSuper)。

方法的调用.png
/** 
 * 发送消息给类的实例对象,并有一个简单的返回信息
 * 
 * @param self  接受这个消息的类的实例对象的指针
 * @param op  处理消息的方法的selector.
 * @note 当发生方法调用的时候,编译器会把方法调用编译为objc_msgSend,  objc_msgSend_stret,  objc_msgSendSuper, objc_msgSendSuper_stret.方法中的一个 
 */
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
/** 
 * 给一个成员变量的父类发送一条消息
 * 
 * @param super  一个objc_super类型的结构体的指针,用该值判定把消息发送给谁 
 * @param op 处理这条消息的方法的selector 
 * @see objc_msgSend
 */
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
(2)、方法的查找过程

方法的查找过程,大概可以分为三个步骤:
a、 查找类的方法缓存cache_t
b、查找类的方法列表cls->data()->methods()
c、递归调用上一级父类,按照a和b的方式,在父类的缓存和方法列表中查找
d、在类及其父类中,没有找到IMP,尝试方法的动态解析过程

下面追个对每个步骤进行说明:

a、 查找类的方法缓存cache_t
在类的缓存中查询使用的是汇编语言,查询过程中更加的快捷高效。
类的方法缓存,查询流程如图所示:

查询类的缓存中的方法.png

_objc_msgSend:方法的实现代码(这里以——arm64——系统进行说明)

//——arm64——
//入口
ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

  //p0是objc_msgSend的第一个参数-消息接收者receiver,判断是不是nil或者tagged pointer
  //cmp语句:比较指令,其内部就是进行减法运算,但不影响值。使用b语句进行跳转
    cmp p0, #0          // nil check and tagged pointer check
  

#if SUPPORT_TAGGED_POINTERS //tagged pointer true amr64
  //上面cmp的语句的结果,如果是小于等于(le)0,那么消息接受这是空或者是tagged pointer,
  //则执行跳转(b)标号为LNilOrTagged的程序
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
  //b.le这句也包含着,如果cmp的结果大于0,则继续执行下面的代码
 
#else
  //cmp指令为0的情况下,即消息接受者为nil,跳转LReturnZero
    b.eq    LReturnZero
#endif
  //ldr:读取指令,从寄存器读取内容的指令
  //x0是self,类偏移0的位置存储的是isa,即把self的isa给到p13
    ldr p13, [x0]       // p13 = isa
  
  //执行 GetClassFromIsa_p16 参数为(isa,1,self)。
  //内部调用ExtractISA,本质是isa & isa_Mask来获得类地址
  //从isa中获取cls
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class   

 
LGetIsaDone://通过上面的过程,根据isa获取到了cls
    //去cls的缓存cache中查询IMP
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    //对cmp结果判断,小于0或者等于0的情况下,都进来
    //再次对cmp的结果进行判断,是不是等于0,即消息接受者为nil。是的话,返回为空
    b.eq    LReturnZero     // nil check
    
  //下面的情况就是接受者的地址指针为tagged pointer
  //内存平移获取index,再去找到cls
    GetTaggedClass
  //找到了cls,跳转到LGetIsaDone标号,执行CacheLookup
    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

根据上述汇编,转的伪代码

_objc_msgSend(void){}
   //检查p0(消息接受者)是不是nil或者tagged pointer。
   //result等于0,p0是nil;result<0,p0是tagged pointer
   result = p0 - #0;

   //如果p0是nil的话,清理寄存器,直接返回空
   if(result == 0){
       //调用LReturnZero
       //返回空
   }

   //p0不是nil的情况下,是否为tagged pointer对于获取cls的方式是不一样的,所以需要对tagged pointer进行判断。
   if(true amr64){
     if(result < 0){//tagged pointer
       //调用GetTaggedClass方法,根据isa获得cls
       GetTaggedClass;
       //调用LGetIsaDone标号,进入CacheLookup方法。在CacheLookup方法中,查询类的cache
       CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
     }else if(result > 0){{
       //调用GetClassFromIsa_p16方法,根据isa获得cls
       GetClassFromIsa_p16 p13, 1, x0  // p16 = class   

       //执行到LGetIsaDone标号,进入CacheLookup方法。在CacheLookup方法中,查询类的cache
       CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
     }
   }else{
       //调用GetClassFromIsa_p16方法,根据isa获得cls
       GetClassFromIsa_p16 p13, 1, x0  // p16 = class   
       //执行到LGetIsaDone标号,进入CacheLookup方法。在CacheLookup方法中,查询类的cache
       CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
   }
 }

根据isa获取cls的方法和过程

根据isa获取cls.png

下面是根据已经找到的cls,去类中cache中,查询缓存方法的流程,调用的是CacheLookup方法。

//在类的方法缓存中通过sel去查找imp
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    // cacheLookup的mode,有NORMAL | GETIMP | LOOKUP
    //   GETIMP:
  //         缓存不存在的话,返回null,x0设置为0
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector 即方法的SEL
    //   - x16 contains the isa 即cls
    //   - other registers are set as per calling conventions
    //

    mov x15, x16            // stash the original isa
LLookupStart\Function:
    // p1 = SEL, p16 = isa

/*****vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv********/
//以下是不同系统中,buckets和mask的取值方式,准备相关数据
//最终获得的结果信息如下
// p10 = buckets
// p11 = mask(arm64真机是_bucketsAndMaybeMask)
// p12 = index 
/***************************************************************/
//arm64 64 OSX/SIMULATOR
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //类的isa地址偏移16个字节,(即isa的8个字节、superClass的8个字节),就得到cache的地址,也就是_bucketsAndMaybeMask
    //存入到p10
    ldr p10, [x16, #CACHE]              // p10 = mask|buckets
      //也就是_bucketsAndMaybeMask低48位是存放方法的空间首地址;高16位是存放mask的空间的首地址
      //p10的第48位,即mask的首地址,存入到p11
    lsr p11, p10, #48           // p11 = mask
      //_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)进行与操作,把高16位置0,得到方法的首地址
      //方法的首地址存放到p10
    and p10, p10, #0xffffffffffff   // p10 = buckets
      //x12 = cmd & mask   w1为第二个参数cmd(self,cmd...),w11也就是p11 
    and w12, w1, w11            // x12 = _cmd & mask

//arm64 64 真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  //类的isa地址偏移16个字节,(即isa的8个字节、superClass的8个字节),就得到cache的地址,也就是_bucketsAndMaybeMask
    //存入到p11,即这里的p11是_bucketsAndMaybeMask
    ldr p11, [x16, #CACHE]          // p11 = mask|buckets
    

    ////arm64 + iOS + !模拟器 + 非mac应用
    #if CONFIG_USE_PREOPT_CACHES

        //iphone 12以后指针验证
        #if __has_feature(ptrauth_calls)
      
            //tbnz 测试位不为0则跳转
          tbnz  p11, #0, LLookupPreopt\Function
            //_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)进行与操作,把高16位置0,得到方法的首地址
            //方法的首地址存放到p10
          and   p10, p11, #0x0000ffffffffffff   // p10 = buckets
        #else
      
            //p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
          and   p10, p11, #0x0000fffffffffffe   // p10 = buckets
            //p11 第0位不为0则跳转 LLookupPreopt\Function。
          tbnz  p11, #0, LLookupPreopt\Function
        #endif
      
                //eor 逻辑异或(^) 格式为:EOR{S}{cond} Rd, Rn, Operand2
            //p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12
        eor p12, p1, p1, LSR #7
            //p12 = p12 & (_bucketsAndMaybeMask >> 48) = p12 & mask = buckets中的下标
        and p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask

    #else
            //_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)进行与操作,把高16位置0,得到方法的首地址
          //方法的首地址存放到p10
          and   p10, p11, #0x0000ffffffffffff   // p10 = buckets
            //p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标
          and   p12, p1, p11, LSR #48       // x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES

//arm64 32
#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
/***************************************************************/
//以上是不同系统中,buckets和mask的取值方式
//最终获得的结果信息如下
// p10 = buckets
// p11 = mask(arm64真机是_bucketsAndMaybeMask)
// p12 = index 
/****^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^*****/
    

      // p13(bucket_t) = buckets + 下标 << 4   PTRSHIFT arm64 为3. 
      // 4位为16字节,所以 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第index个元素的地址。 
      //这里p13为buckets中间的一个bucket的寄存器
      //所以这里的do-while是一个p13不断前向遍历的过程,遍历到bucket的首地址是停止。即这个do-while是遍历的buckets的前半部分
 
    add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

                        // do {
    //ldp 寄存器1 寄存器2 寄存器3 #xx:读取寄存器3 --> 内存地址 --> 内存数据,按字节顺序放入寄存器1和寄存器2中。然后寄存器3的内存地址 - xx
    //把x13(bucket)的imp和sel读取出来,依次放入p17、p9寄存器中,并且x13的内存地址减小一个bucket的大小
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
    cmp p9, p1              //     if (sel != _cmd) {
    b.ne    3f              //         scan more
                        //     } else {
2:  CacheHit \Mode              // hit:    call or return imp
                        //     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
    cmp p13, p10            // } while (bucket >= buckets)
  //b.hs指令是判断是否无符号小于
    b.hs    1b

  
  
  
//在buckets的前半部分[0 - index]没有找到。继续在后半部分机进行查找(index - (buckets.count-)]
  
/*****vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv********/
//以下是不同系统中根据mask获取最后一个bucket地址,即准备数据
//p13 = 最后一个bucket的地址
#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
  //p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。
  //因为maskZeroBits的存在 就找到了mask对应元素的地址 这里没搞明白
    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
/****^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^*****/
  
  
  // 根据上面 p13 = 最后一个bucket的地址,也是查询的有边界
  //p12 = buckets + (p12<<4) index对应的bucket_t ,也就上此查询的开始地址,用来做查询的左边界
  //(p12 p13];
    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)
  //如果查询到了,跳到2标号,cacheHit
    b.eq    2b              //         goto hit
  
  //循环条件,p12 p13都有值,且p13 > p12
    cmp p9, #0              // } while (sel != 0 &&
    ccmp    p13, p12, #0, ne        //     bucket > first_probed)
  //符合循环条件,继续循环执行
    b.hi    4b

  //遍历全部的buckets,没找到imp,跳转到 __objc_msgSend_uncached
LLookupEnd\Function:
LLookupRecover\Function:
    b   \MissLabelDynamic

    /*其他代码*/
    
.endmacro

根据上述流程写出的伪代码,如下:

//CacheLookup的伪代码
CacheLookup(Mode, Function, MissLabelDynamic, MissLabelConstant){
  
  //1、各个系统架构下,获取到buckets、mask、并且处理后得到遍历的标识位index等信息,
  p10 = buckets;
  p11 = mask;//(arm64真机是_bucketsAndMaybeMask)
  p12 = index;
  
  
  /***2、buckets中前半部分的遍历操作[0, index]***/
  // p13 = buckets + index *16;
  //获取到第index个bucket的地址,并且赋值到p13寄存器中
  p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT));
  do{
    //把p13存放的bucket的imp赋值给p17
    p17 = p13.imp;
    //把p13存放的bucket的sel赋值给p9
    p9 = p13.sel;
    //即p13的地址向前移一个bucket大小,即p13改为前一个bucket的地址
    p13 = p13 - BUCKET_SIZE;
    
    
    //p1是要查找的方法的SEL 
    //查找到了方法的IMP。跳转到CacheHit 传值为 NORMAL
    if(p9 == p1){
      //调用cacheHit方法
      CacheHit NORMAL
        break;
    }
    
    //没有找到的情况下
    if(p9 == nil){
      //证明cache有问题,跳转到 __objc_msgSend_uncached
      __objc_msgSend_uncached
        break;
    }
  }while(p13 >= p10);//即前向遍历,p13的地址到了第一个bucket(p10)的地址
  
  
  /***3、在buckets的前半部分没有找到方法的IMP,需要查找buckets的后半部分(index, buckets.count]***/
  //各个系统架构下,取得mask位的地址,获得buckets最后的bucket的地址,赋值给p13.用来做查询的右边界
    p13 = buckets.count - 1;
  //p12 index对应的bucket_t ,也就上此查询的开始地址,用来做查询的左边界
  p12 =  buckets + (p12<<4);
  do{
    //把p13存放的bucket的imp赋值给p17
    p17 = p13.imp;
    //把p13存放的bucket的sel赋值给p9
    p9 = p13.sel;
    //即p13的地址向前移一个bucket大小,即p13改为前一个bucket的地址
    p13 = p13 - BUCKET_SIZE;
    
    //p1是要查找的方法的SEL 
    //查找到了方法的IMP。跳转到CacheHit 传值为 NORMAL
    if(p9 == p1){
      //调用cacheHit方法
      CacheHit NORMAL
        break;
    }
  }while((p9 != nil) && (p13 > p12);//循环条件,p12 p13都有值,且p13 > p12,p9不为nil
  
         
         
  /***4、遍历全部的buckets,没有找到方法的IMP,跳转到 __objc_msgSend_uncached***/
  __objc_msgSend_uncached;
}

找到IMP后的实现流程

找到IMP后的调用流程.png
方法列表、查询父类、动态查询相关代码

在当前类的方法缓存中cache没有找到方法的对应IMP。那么接下来就是要到类的方法列表中,遍历方法列表。
上面的图能显示出来,缓存中找不到方法的情况下,会调用__objc_msgSend_uncached方法,而这个方法最终会调用到IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)。这个方法中,使用for循环去查询类的缓存或者类的方法列表,并且在上述查不到的情况下,递归调用父类,查询父类的缓存和方法列表。直到找到IMP,或者特殊情况的break,才能跳出for 循环。
下面是IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)方法的源码,查找类的方法列表、查询父类以及方法的动态解析都在这个方法中。

NEVER_INLINE
//在MethodTableLookup方法中,传进来的behavior = 3
//LOOKUP_INITIALIZE | LOOKUP_RESOLVER // LOOKUP_INITIALIZE = 1;LOOKUP_RESOLVER = 2
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
   //forward_imp赋值
   const IMP forward_imp = (IMP)_objc_msgForward_impcache;
   //要返回的imp
   IMP imp = nil;
   //当前查找的cls
   Class curClass;

   runtimeLock.assertUnlocked();
 
   //如果类没有初始化,behavior会增加 LOOKUP_NOCACHE。
   if (slowpath(!cls->isInitialized())) {
       //behavior = 3// 011
       //LOOKUP_NOCACHE = 8// 1000
       //behavior |= LOOKUP_NOCACHE  = 0011 | 1000 = 1011 
       behavior |= LOOKUP_NOCACHE;
   }

   //加锁
   runtimeLock.lock();
 
   //在缓存内、loaded image的数据段内或已使用obj_allocateClassPair分配,则返回true
   checkIsKnownClass(cls);

   //如果类没有初始化,则对类进行初始化。
   //在类的初始化过程中,完成对rw、ro的准备工作,并且对父类及其原类进行数据准备
   //这样也为后面的查询,提供了数据信息
   cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
 
   // 解锁
   runtimeLock.assertLocked();
   
   //赋值要查找的类
   curClass = cls;
 
           /*--循环开始--*/
   //在上限范围内循环,除非return/break
   //Provides an upper bound for any iteration of classes,  to prevent spins when runtime metadata is corrupted.
   for (unsigned attempts = unreasonableClassCount();;) {
       
       //先去缓存查找,防止这个时候共享缓存中已经写入了该方法。
       if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES //defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
           //iOS
           
           //这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找缓存
           imp = cache_getImp(curClass, sel);
           //找到后直接跳转done_unlock
           if (imp) goto done_unlock;
           //这个操作是要做什么?
           curClass = curClass->cache.preoptFallbackClass();
#endif
       } else {
           // curClass method list.
           // curClass method list.查找类的方法列表,cls->data()->methods()
           Method meth = getMethodNoSuper_nolock(curClass, sel);
           if (meth) {
             //找到了IMP,跳到done,插入缓存
               imp = meth->imp(false);
               goto done;
           }

           
           //这里curClass 会被赋值为上一级父类,
           //也就是自己类没有查找到,就去查找父类的方法列表
           //如果到了NSObject的父类,也就是nil,就赋值imp为foreard_imp。break出循环
           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.");
       }

       // 递归调用父类的查询机制,显示缓存再是方法裂列表
       //_cache_getImp --> CacheLookup --> __objc_msgSend_uncached -->MethodTableLookup --> _lookUpImpOrForward
       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;
     
        //该方法在没有找到动态解析的方法时,也是会返回nil。
       //这样就得进入到消息转发流程了
       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
       //把方法存储到缓存中。方法的IMP,是在本次for结束后,下次for开始时查询类的缓存中进行返回的
     //也就是在类的方法列表,或者在父类中找到的方法IMP,都要在done_unlock中返回
       log_and_fill_cache(cls, imp, sel, inst, curClass);
   }
done_unlock:
   runtimeLock.unlock();
   if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
       return nil;
   }
   //返回方法的IMP
   return imp;
}

该方法的查找流程如下图所示:
lookUpImpOrForward.png

b、查找类的方法列表cls->data()->methods()
在类的方法列表中查询IMP的代码如下:

  // curClass method list.
  // curClass method list.查找类的方法列表,cls->data()->methods()
  Method meth = getMethodNoSuper_nolock(curClass, sel);
  if (meth) {
      imp = meth->imp(false);
      goto done;
   }

调用static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)方法进行的查询

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

最终会调用到static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)方法,该方法使用二分法对method_list_t进行遍历。

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;

  //目标sel所在的位置
  uintptr_t keyValue = (uintptr_t)key;
  //方法列表中的数量
  uint32_t count;
  
  //使用二分法查询
  //count >>= 1 count右移以一位,相当于count / 2.
  for (count = list->count; count != 0; count >>= 1) {
      //计算probe查询位置
      probe = base + (count >> 1);
      
      //获取到查询位置的sel,与keyValue对比
      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;
      }
      
      //probe位置的sel不是要查询的sel,并且目标sel的位置比probe要大
      if (keyValue > probeValue) {
          base = probe + 1;
          count--;
      }
      
      //probe位置的sel不是要查询的sel,如果目标sel的位置小于等于probe的话,进入到下一次for循环,
      //即count>>1后,再次计算probe
  }
  
  return nil;
}

下面,我们带入数字走一遍查询流程:
假设现在的method_list_t中右10条方法,则list->count = 10,并且假设目标sel的位置在第3位,那么首次遍历的初始条件为:
第1次: base = 0; count = 10; keyValue = 3;
>>> probe = base + count>>1 = 0 + 10 / 2 = 5;
>>>> 不是要找的sel,并且keyvalue<probe,不做操作。
>>>>>在for结束时执行count>>1,即:count = 10>>1 = 5;
>>>>>>综上,得出下一次的相关数据:base = 0; count = 5; keyValue = 3;

第2次:base = 0; count = 5; keyValue = 3;
>>> probe = base + count>>1 = 0 + 5 / 2 = 2;
>>>> 不是要找的sel,并且keyvalue > probe,执行 base = probe+1; count--;操作。即:base = 2+1 = 3;count = 5-1=4
>>>>> 在for结束时执行count>>1,即:count = 4>>1 = 2;
>>>>>>综上,得出下一次的相关数据:base = 3; count = 2; keyValue = 3;

第3次:base = 3; count = 2; keyValue = 3;
>>> probe = base + count>>1 = 3 + 2 / 2 = 4;
>>>> 不是要找的sel,且keyvalue < probe,不做操作。
>>>>> 在for结束时执行count>>1,即:count = 2>>1 = 1;
>>>>>>综上,得出下一次的相关数据:base = 3; count = 1; keyValue = 3;

第4次:base = 3; count = 1; keyValue = 3;
>>> probe = base + count>>1 = 3 + 1 / 2 = 3;
>>>> 该位置就是要查询的sel,查找结束。

在查询流程结束后,会进入到一个while循环,来查看当前位置之前,是不是有相同SEL的方法。如果有的话,调用前面的IMP。这种情况发生在有分类并且分类中也实现了相同的方法。分类的方法是在类的前面的,优先调用分类中的方法。

查找更前面位置的方法.png

c、递归调用上一级父类,按照a和b的方式,在父类的缓存和方法列表中查找

  • 1、在方法的列表中没有找到方法IMP,把curClass->superClass赋值给curClass
  • 2、调用“cache_getImp”方法
  • 3、cache_getImp是要调用“CacheLookup”,这样又走了一遍汇编查询cache、查询类的方法列表、查询父类方法列表的流程。
  • 4、因此在这里进行了递归调用
  • 5、一旦在父类中查询到了方法的实现,需要在方法接受类的缓存中添加该方法。
  • 6、在下一次的for循环中,从cache中读取并调用IMP。
递归调用父类.png

d、在类及其父类中,没有找到IMP,尝试方法的动态解析过程
目标sel的方法实现imp没找到,系统会再给一次补救机会:动态方法解析。
下面两个方法,是实现动态解析的实现,在方法的实现中,可以动态的为sel添加IMP
实例方法:+ (BOOL)resolveInstanceMethod:(SEL)sel;
类方法:+ (BOOL)resolveClassMethod:(SEL)sel;

在查询类的方法缓存以及方法列表,没有查找到方法实现IMP。会进入到动态解析流程
动态解析流程.png

动态解析使用举例:

/*======================================*/
/*********--MethodSend的类声明--**********/
/*=====================================*/

@interface MethodSend : NSObject

//声明实例方法,但不实现该方法
-(void)instanceMethodResolveTest;
//声明类方法,但不实现该方法
+(void)classMethodResolveTest;

//声明一个实例方法,但不实现该方法,并且让另外一个类实现动态解析
-(void)otherCls_InstanceMethodResolveTest;

@end

/*======================================*/
/*********--MethodSend的类实现--**********/
/*=====================================*/
#import "Dog.h"
#import "MethodSend.h"
#include <objc/runtime.h>

@implementation MethodSend
/*--类方法的动态解析--*/
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(classMethodResolveTest)) {
        class_addMethod(object_getClass(self),
                        sel,
                        class_getMethodImplementation(object_getClass(self), @selector(dynamicClassIMP)),
                        "v@:");
        return true;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}
+(void)dynamicClassIMP{
    NSLog(@"类方法--classMethodResolveTest--的动态解析:IMP");
}


/*--对象方法的动态解析--*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(instanceMethodResolveTest)) {
        class_addMethod([self class],
                        sel,
                        class_getMethodImplementation([self class],
                                                      @selector(dynamicInstanceIMP)),
                        "v@:");
        return true;
    }else if (sel == @selector(otherCls_InstanceMethodResolveTest)) {
        class_addMethod([self class],
                        sel,
                        class_getMethodImplementation([Dog class], @selector(dog_dynamicInstanceResolve)),
                        "v@:");
        return true;
    }
   
    return [super resolveInstanceMethod:sel];
}

-(void)dynamicInstanceIMP{
    NSLog(@"实例方法--instanceMethodResolveTest--的动态解析:IMP");
}

@end


/*====================================================================*/
/*********--在Dog类中实现“otherCls_InstanceMethodResolveTest”--**********/
/*====================================================================*/
#import "Dog.h"
@implementation Dog

- (void)dog_dynamicInstanceResolve{
    NSLog(@"~~~~我是“Dog”类的动态解析方法~~~");
}

@end

打印结果如下:

2022-07-31 11:58:42.412965+0800 schemeUse[3546:138093] 实例方法--instanceMethodResolveTest--的动态解析:IMP
2022-07-31 11:58:42.413031+0800 schemeUse[3546:138093] 类方法--classMethodResolveTest--的动态解析:IMP
2022-07-31 11:58:42.413075+0800 schemeUse[3546:138093] ~~~~我是“Dog”类的动态解析方法~~~

3、消息转发

动态方法解析失败后流程会被标记,随即再次触发消息查询,此时会跳过动态方法解析流程直接进行消息转发。
所谓消息转发,是将当前消息转发到其它对象进行处理。(这里可以做出类的虚假的多继承,即一个类继承了多个类,使用每个类中的方法)

(1)、快速转发

转发实例方法: - (id)forwardingTargetForSelector:(SEL)aSelector
转发类方法,id需要返回类对象: + (id)forwardingTargetForSelector:(SEL)sel
定义在NSObject类中,默认返回nil

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

举例如下:

/*======================================*/
/*********--MethodSend的类声明--**********/
/*=====================================*/
@interface MethodSend : NSObject

//声明实例方法,但不实现该方法
-(void)instanceMethodResolveTest;
//声明类方法,但不实现该方法
+(void)classMethodResolveTest;

@end


/*======================================*/
/*********--MethodSend的类实现--**********/
/*=====================================*/
@implementation MethodSend

//快速forward,实例对象的消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(instanceMethodResolveTest)) {
        //转发到Dog类,让Dog类的instanceMethodResolveTest方法,进行实现
        return [[Dog alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}


//快速forward,类方法的消息转发
+ (id)forwardingTargetForSelector:(SEL)sel {
    if (sel == @selector(classMethodResolveTest)) {
        //转发类方法,id需要返回类对象
        return  NSClassFromString(@"Dog");
    }
    return [super forwardingTargetForSelector:sel];
}

@end




/*======================================*/
/*********--Dog的类声明--**********/
/*=====================================*/
@interface Dog : NSObject
//对象方法
-(void)instanceMethodResolveTest;
//类方法
+(void)classMethodResolveTest;
@end


/*====================================================================*/
/*********--在Dog类中实现对象方法“instanceMethodResolveTest”--**********/
/************--在Dog类中实现类方啊“classMethodResolveTest”--************/
/*====================================================================*/
@implementation Dog
-(void)instanceMethodResolveTest{
    NSLog(@"instanceMethodResolveTest 在 Dog 中实现了");
}
+(void)classMethodResolveTest{
    NSLog(@"classMethodResolveTest 在 Dog 中实现了");
}
@end

打印结果如下

    MethodSend *ms = [[MethodSend alloc] init];
    //实例方法消息转发
    [ms instanceMethodResolveTest];
    //类方法的消息转发
    [MethodSend classMethodResolveTest];


2022-08-01 17:30:24.147594+0800 schemeUse[15671:253255] instanceMethodResolveTest 在 Dog 中实现了
2022-08-01 17:30:24.147658+0800 schemeUse[15671:253255] classMethodResolveTest 在 Dog 中实现了
(2)、慢速转发

如果forwardingTargetForSelector没有实现,或返回了nil或self,则会进入另一个转发流程。

它会依次调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,然后runtime会根据该方法返回的值,组成一个NSInvocation对象。如果返回的方法签名为nil,则直接崩溃报错。如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也不会报错

再去调用- (void)forwardInvocation:(NSInvocation *)anInvocation。注意,当调用到forwardInvocation时,无论我们是否实现了该方法,系统都默认消息已经得到解析,不会引起crash。


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(instanceMethodResolveTest)) {

        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
}
(3)、doesNotRecognizeSelector

如果没有方法的动态解析过程呢,也没有消息转发过程的话,则直接调用 doesNotRecognizeSelector抛出异常信息。

(4)、图示转发流程
消息转发流程.png

4、调用super的方法

前面图中说过,调用super方法的时候,在编译阶段,会编译成 void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )这种形式。并以这种形式完成消息的传递。
调用父类方法的格式为[super xxxx: xx], 其中super会被编译成objc_super *super结构体。也就是objc_msgSendSuper的第一个参数。
objc_super *super结构如下:

/// Specifies the superclass of an instance. 
struct objc_super {
  /// Specifies an instance of a class.
  //类的实例变量,也是实际消息的接受者。
  //即使使用super的方法,该父类方法的消息最终还是要发送给调用super方法所在的类的实例对象
  __unsafe_unretained _Nonnull id receiver;

  //下面是对象>类>superClass  ,以super
  /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
  /* For compatibility with old objc-runtime.h header */
  __unsafe_unretained _Nonnull Class class;
#else
  __unsafe_unretained _Nonnull Class super_class;
#endif
  /* super_class is the first class to search */
};

当调用super method时,runtime会到super class中找到IMP,然后发送到当前class的实例上。


_objc_msgSendSuper.png

5、几个特殊方法

1、classobject_getClass
实例对象的class方法

- (Class)class方法内部直接返回的是object_getClass(self),所以实例对象的classobject_getClass是一样的。

- (Class)class {
    return object_getClass(self);
}

示例代码:从打印结果和地址的输出,可以看出两者是同样的地址

- (void)instanceMethod{
    Class cls_Clazz = [self class];
    Class cls_Objc = object_getClass(self);
    NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}

//打印结果为
2022-08-02 17:28:18.933562+0800 schemeUse[6677:286665] cls_Clazz is MethodSend -- cls_Objc is MethodSend

//对cls_Clazz和cls_Objc 输出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000104fdd380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000104fdd380 MethodSend
类对象的class方法:

+ (Class)class方法内部,返回了self。而如果在类方法中调用object_getClass,其实返回的是元类。所以在类方法中,使用这两个函数,是完全不同的。

+ (Class)class {
    return self;
}

示例代码:从打印结果和地址的输出,打印出来的类是一样但是两者地址是不同的。

+ (void)classMethod{
    Class cls_Clazz = [self class];
    Class cls_Objc = object_getClass(self);
    NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}

//打印结果为
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend

//对cls_Clazz和cls_Objc 输出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358
2、isMemberOfClass
实例方法:

用于判断当前对象所在的类是不是和目标类cls相同

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
类方法:

用于判断当前类对应的元类是不是和目标类cls相同

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
3、isKindOfClass
实例方法:

用于判断对象的类或者父类 ,有能与cls匹配即返回true。

- (BOOL)isKindOfClass:(Class)cls {
    //取出对象的类,并遍历自己的父类,只要能和cls匹配,就返回true
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}
类方法:

用于判断当前类对应的的元类及元类的父类,只要能和cls匹配,就返回true

+ (BOOL)isKindOfClass:(Class)cls {

    //当前self为类,类的isa指向为元类
    //取出当前类的元类,并遍历元类的父类,只要能和cls匹配,就返回true
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}
4、isKindOfClassisMemberOfClass的面试题

isKindOfClassisMemberOfClass的面试题,结合 isa指针指向:

BOOL result1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL result2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL result3 = [[Person class] isKindOfClass:[Person class]];
BOOL result4 = [[Person class] isMemberOfClass:[Person class]];
BOOL result5 = [[Person class] isKindOfClass:[NSObject class]];

得出的结果为:

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

推荐阅读更多精彩内容