Runtime底層原理探究(一) --- 訊息轉發機制(快速轉發)

派出所發表於2019-04-01

OC是一門動態語言所有的方法都是由執行時進行。 Objective-C 語言將決定儘可能的從編譯和連結時推遲到執行時。只要有可能,Objective-C 總是使用動態 的方式來解決問題。這意味著 Objective-C 語言不僅需要一個編譯器,同時也需要一個執行時系統來執行 編譯好的程式碼。這兒的執行時系統扮演的角色類似於 Objective-C 語言的作業系統,Objective-C 基於該系統來工作。 Runtime的作用是 能動態產生/修改一個類,一個成員變數,一個方法

Runtime呼叫有三種方式

  1. NSObject(peformselector)
  2. Selector(底層會轉換為objc_msgSend())
  3. Runtime的Api

Runtime底層原理探究(一) --- 訊息轉發機制(快速轉發)

objc_msg_send()

我們知道OC的函式呼叫是訊息傳送機制,那麼訊息傳送機制是如何實現的呢。

Animals * animal = [[Animals alloc]init];
[animal eat];
複製程式碼

將該檔案編譯成c++檔案通過
clang-rewrite-objc 檔名 -o test.c++
命令 一共9w多行程式碼只需看最後

// -(void) eat;
/* @end */

#pragma clang assume_nonnull end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_b2_hs7ds2bd5zz7d752kk495bhw0000gn_T_main_f668c6_mi_0);

        Animals * animal = ((Animals *(*)(id, SEL))(void *)objc_msgSend)((id)((Animals *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animals"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("eat"));

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼

objc_msgSend(void /* id self, SEL op, ... */ ) 當類初始化時候 顯示獲取 id self,即 (id)objc_getClass("Animals"),就是根據類名取獲取這個類,然後alloc,init就是 #selector(alloc) 其底層實現是 sel_registerName("alloc/init"),其目的就是為了查詢該類裡面有沒該方法 第二句同理target是已經生產的animal selector是 eat方法 sel_registerName("eat")去類的記憶體佈局中查詢eat方法

objc_msgsend 底層實現有兩種方法一中是快速轉發一種是慢速轉發 快速是通過彙編從響應的快取裡面找到,慢速是通過c,c++以及彙編一起完成的。

之所以使用匯編的原因是 :

  1. c裡面不會寫一個函式保留未知的引數跳轉到任意的指> 針,c無法實現,彙編可以通過暫存器直接實現
  2. 快,下層編譯

快速轉發

快速轉發直接通過 彙編 + 快取 來進行查詢的 快取是來自於類、

///ps: 類繼承於物件從這裡也可以看出來類其實也是一個物件
struct objc_class: objc_objcet {
  // class ISA;
  Class superclass;
  cache_t cache; /// 
  classs_data_bits_t bitgs; /// 類裡面所有的資料
  
  class_rw_t *data() {
      return bits.data()
  }
}
複製程式碼

類結構裡的 cacle_t 快取 儲存方法的Selector(在iOS中SEL就是可以根據一個SEL選擇對應的方法IMP。SEL只是描述了一個方法的格式)IMP(一個函式指標,這個被指向的函式包含一個接收訊息的物件id(self 指標), 呼叫方法的選標 SEL (方法名),以及不定個數的方法引數,並返回一個id。也就是說 IMP 是訊息最終呼叫的執行程式碼,是方法真正的實現程式碼 。我們可以像在C語言裡面一樣使用這個函式指標。)。IMP和Selector會組成一張雜湊表,通過雜湊直接查詢非常快,當查詢第一個方法的時候第一步找到cache,如果裡面有他會直接返回。如果沒有會經歷一個複雜的過程(慢速查詢)。找到了會在裡面存一份方便下次進行查詢,這次主要介紹快速找找的過程通過OC原始碼

剛剛的方法通過Xcode除錯除錯彙編頁面

Runtime底層原理探究(一) --- 訊息轉發機制(快速轉發)

在原始碼裡搜尋_objc_msgsend

Runtime底層原理探究(一) --- 訊息轉發機制(快速轉發)

先把完整的彙編原始碼貼上,可以往下看,然後在回來看

********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17
 * x16 reserved for our use but not used
 *
 ********************************************************************

    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0

    ENTRY _objc_msgSend ///************************************** 1.進入objcmsgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
    /// x0 recevier
    // 訊息接收者  訊息名稱
    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) //// ****************************************************2.isa 優化
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone: ///**************************************************** 3.isa優化完成
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached ///*******************************************4.執行 CacheLookup NORMAL

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    /// tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    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]
    b   LGetIsaDone

LExtTag:
    // 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
    
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend


    ENTRY _objc_msgLookup
    UNWIND _objc_msgLookup, NoFrame

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LLookup_NilOrTagged //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LLookup_GetIsaDone:
    CacheLookup LOOKUP      // returns imp

LLookup_NilOrTagged:
    b.eq    LLookup_Nil // nil check

    /// tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LLookup_ExtTag
    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]
    b   LLookup_GetIsaDone

LLookup_ExtTag: 
    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   LLookup_GetIsaDone

LLookup_Nil:
    adrp    x17, __objc_msgNil@PAGE
    add x17, x17, __objc_msgNil@PAGEOFF
    ret
    END_ENTRY _objc_msgLookup
    STATIC_ENTRY __objc_msgNil
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret
    
    END_ENTRY __objc_msgNil


    ENTRY _objc_msgSendSuper
    UNWIND _objc_msgSendSuper, NoFrame
    MESSENGER_START

    ldp x0, x16, [x0]       // x0 = real receiver, x16 = class
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

    END_ENTRY _objc_msgSendSuper

    // no _objc_msgLookupSuper

    ENTRY _objc_msgSendSuper2
    UNWIND _objc_msgSendSuper2, NoFrame
    MESSENGER_START

    ldp x0, x16, [x0]       // x0 = real receiver, x16 = class
    ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
    CacheLookup NORMAL

    END_ENTRY _objc_msgSendSuper2

    
    ENTRY _objc_msgLookupSuper2
    UNWIND _objc_msgLookupSuper2, NoFrame

    ldp x0, x16, [x0]       // x0 = real receiver, x16 = class
    ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
    CacheLookup LOOKUP

    END_ENTRY _objc_msgLookupSuper2


.macro MethodTableLookup
    
    // push frame
    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)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3/// *********************************************6.方法為_class_lookupMethodAndLoadCache3呼叫的組合語言

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

.endmacro

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup /// ********************************************** 5.查詢IMP
    br  x17

    END_ENTRY __objc_msgSend_uncached


    STATIC_ENTRY __objc_msgLookup_uncached
    UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup
    ret

    END_ENTRY __objc_msgLookup_uncached


    STATIC_ENTRY _cache_getImp

    and x16, x0, #ISA_MASK
    CacheLookup GETIMP

LGetImpMiss:
    mov x0, #0
    ret

    END_ENTRY _cache_getImp
複製程式碼
  1. LLookup_NilOrTagged///針對記憶體裡暫存器進行賦值處理isa優化。
  2. LGetIsaDone isa /// isa處理完畢
  3. CacheLookup Normal 呼叫當前imp或者傳送objcmsgsend_uncache

1.CacheLookup Normal

先貼原始碼

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *   x1 = selector
 *   x16 = class to be searched
 *
 * Kills:
 *   x9,x10,x11,x12, x17
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.macro CacheHit /// cachehit
.if $0 == NORMAL /// normal ///call imp
    MESSENGER_END_FAST
    br  x17         // call imp
.elseif $0 == GETIMP
    mov x0, x17         // return imp
    ret
.elseif $0 == LOOKUP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz x9, LGetImpMiss
.elseif $0 == NORMAL
    cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz x9, __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

.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          /// loop

3:  // wrap: x12 = first bucket, w11 = mask 
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          /// loop

3:  // double wrap
    JumpMiss $0
    
.endmacro
複製程式碼

CacheLookup 有三種 NORMAL GETIMP LOOKUP

  1. CacheHit call imp /// 查詢快取
  2. CheckMiss - /// 找不到的處理 傳送_objc_msgSend_uncached
  3. add /// 如果快取裡沒有找到但是其他地方找到了這時候就可以add到快取裡面去

CacheHit 也是一個巨集,如果$0 == Normal 則進行call imp 操作這是找到了操作。如果找不到的話,則執行check miss,check miss也是一個巨集 $0 == Normal 會傳送 objcmsgsend_uncache,這個時候整個流程就出來了。 CacheHit的意義就是要麼查詢IMP要麼傳送objcmsgsenduncache方法

2. _objc_msgSend_uncache

如果走到這裡說明CacheHit並沒有找到對應的方法而執行了_objc_msgSend_uncache /// 沒有快取去慢速查詢imp

  STATIC_ENTRY __objc_msgSend_uncached
  UNWIND __objc_msgSend_uncached, FrameWithNoSaves

  // THIS IS NOT A CALLABLE C FUNCTION
  // Out-of-band x16 is the class to search
  
  MethodTableLookup /// 重點 方法列表
  br  x17  // call imp 

  END_ENTRY __objc_msgSend_uncached


  STATIC_ENTRY __objc_msgLookup_uncached
  UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

  // THIS IS NOT A CALLABLE C FUNCTION
  // Out-of-band x16 is the class to search
  
  MethodTableLookup
  ret

  END_ENTRY __objc_msgLookup_uncached
複製程式碼

MethodTableLookup 方法列表這個方法是關鍵, 因為 br x 17 是設定imp,而 MethodTableLookup 在之前呼叫說明他是在慢速查詢。

3. MethodTableLookup

.macro MethodTableLookup
  
  // push frame
  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)]

  // receiver and selector already in x0 and x1
  mov x2, x16
  bl  __class_lookupMethodAndLoadCache3 /// 重點查詢IMP

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

.endmacro
複製程式碼

__class_lookupMethodAndLoadCache3 這個方法顧名思義是 查詢方法列表並快取,到了這裡了我們發現原始碼裡面並沒有看到這個方法的定義。因為

__class_lookupMethodAndLoadCache3 方法為_class_lookupMethodAndLoadCache3呼叫的組合語言 通過_class_lookupMethodAndLoadCache3 來到c++檔案

Runtime底層原理探究(一) --- 訊息轉發機制(快速轉發)

快速轉發總結

oc的方法呼叫本質是進行objc _ msgSend呼叫,而objcmsgSend進行實現的時候有兩種方式一種是快速查詢一種是慢速查詢。快速查詢是oc先去類結構裡的cache_ t的類面去查詢,裡面是由 c c++ 和彙編一起完成的,採用會變得原因是他可以做到c語言無法完成的原因是c裡面不會寫一個函式保留未知的引數跳轉到任意的指標,c無法實現,彙編可以通過暫存器直接保留,而且速度快,進入objc_ msg_ send的時候

  1. 首先會執行LLookup_NilOrTagged對isa進行優化,優化完畢後會執行LLookup_GetIsaDone.
  2. 之後執行CacheLookup進行快取查詢,快取查詢會分三步CacheHit如果是Normal,則直接返回imp說明快取中有並返回(br x17),如果是GETIMP,LOOKUP,則會繼續往下走。如果沒有的話則執行checkmiss,Normal LOOKUP操作 傳送objc_msgSend_uncahced訊息,objc_msgSend_uncahced進入該方法後說明沒有快取進入慢速查詢IMP,裡面有一個MethodTableLookup,之所以這句是關鍵程式碼適應br x 17是為imp進行設定,所以該程式碼是關鍵,這方法裡面執行**__class_lookupMethodAndLoadCache3**,因為該方法是**_class_lookupMethodAndLoadCache3**呼叫的組合語言所以就查詢到這個方法,該方法是c++檔案裡的程式碼

到了這裡就已經進入到c++ 檔案裡面。下篇文章具體分析慢速轉發流程。

ps:以上為個人理解,如果有誤歡迎指正。一起進步

相關文章