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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容