iOS 底層探索篇 —— 方法的查找流程

前言c++

消息發送的本質

1. 咱們在main.m文件裏面定義一個sayHello方法

int main(int argc, const char * argv[]) {
        @autoreleasepool {
        XDPerson *person = [XDPerson alloc];
        [person sayHello];
      }
      return 0;
}
複製代碼

2. clang這個main.m文件以後,咱們能夠在相應的main.cpp文件裏面能夠看到

int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
          XDPerson *person = ((XDPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XDPerson"), sel_registerName("alloc"));

          ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
        }
        return 0;
}
複製代碼

3. 咱們把第2步的main函數裏面的sayHello函數抽出來而後精簡一下

objc_msgSend(person,sel_registerName("sayHello"));算法

結論:緩存

  • 消息發送的本質就是調用objc_msgSend函數;
  • objc_msgSend函數的參數,對象(或者是類對象) + 方法名。

objc_msgSend的查找流程

objc_msgSend查找流程由如下兩部分組成:bash

  • 快速查找流程;
  • 慢速查找流程;

快速查找流程

1. 查找主線流程

  1. 彙編斷點查看,能夠看到在彙編底層在call objc_msgSend
XDTest`main:
    0x100000d40 <+0>:   pushq  %rbp
    ...省略部分信息...
    0x100000d8b <+75>:  callq  *0x27f(%rip)   ; (void *)0x0000000100343bc0: objc_msgSend
    ...省略部分信息...
    0x100000db0 <+112>: popq   %rbp
    0x100000db1 <+113>: retq  
複製代碼
  1. 全局搜索能夠再objc-msg-arm64.s文件中能夠看到ENTRY objc_msgSend,查找從這裏就開始了。
ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa                   
    GetClassFromIsa_p16 p13     // p16 = class         
LGetIsaDone:
    CacheLookup NORMAL
複製代碼

主線流程介紹:多線程

  • cmp p0, #0檢查是否爲空,或者是否taggedPointerSUPPORT_TAGGED_POINTERS__LP64__宏定義的值爲1。 b.le LNilOrTagged實質上就是去check,可是咱們知道確定不會爲Nill,而且是nontaggedpointer(後面會介紹)。
  • ldr p13, [x0]x0就是isa給到p13
  • GetClassFromIsa_p16 p13 顧名思義就是經過p13找到class
  • LGetIsaDone完成了以後 就開始CacheLookup,傳入的參數是NORMAl。

2. 主線流程深刻

  1. GetClassFromIsa_p16 p13探索
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
    ...省略部分信息...
#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK
#else
    ...省略部分信息...
#endif
.endmacro
複製代碼
  • SUPPORT_INDEXED_ISA查找宏定義就能夠知道值爲0。
  • and p16, $0, #ISA_MASK實際的入口就是這裏。意思就是將第一個參數(即上面傳入的isa)&ISA_MASK 以後賦值給p16,這就很明顯的取到了咱們的目標類(元類)了。
  1. LGetIsaDoneisa咱們已經完成,開始CacheLookup入參NORMAL
.macro CacheLookup        
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

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

3:  // wrap: p12 = first bucket, w11 = mask              
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

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

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

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

看到這裏咱們頭皮發麻,不要慌,咱們開始對第2階段一步一步分析。函數

2.1 ldp p10, p11, [x16, #CACHE],咱們經過平移指針的地址16個字節以後能夠找到objc_class結構體裏面的cache_t指針。這裏能夠經過iOS 底層探索篇 —— isa的初始化&指向分析分析這個章節看到結構體的相關信息與介紹。oop

#CACHE是一個宏定義且值爲8,而後一分爲兩個內存段。post

  • 0000 1222 就給p10實際上就是buckets指針賦值。
  • 0023 0033 就給p11實際上就是occupied|mask賦值。mask屬性在結構體裏面是在occupied屬性的前面,可是賦值在後面的緣由是由於iOS是小端模式

2.2 經過hash函數、平移、取值一列操做,找打當前sel對應hash表裏面的impui

and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
複製代碼

2.3 接下來的流程就和cache_t裏面的查找流程相同,找不到就開始遞歸查找,直到找到爲止。上面咱們能夠看到會有兩次123這樣的流程,第二次123就是防止多線程調用的時候給的一次容錯機會。this

2.4 咱們主要看cacheHit cacheMiss JumpMiss

  • cacheHit 緩存命中,表示找到了sel對應的imp,直接返回imp
  • cacheMiss當前對應的bucket沒找到,繼續遞歸查找。
  • JumpMiss遞歸查找介紹,沒有緩存命中,跳出當前流程。

2.5 JumpMiss 流程 參數$0就是NORMAL

.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
複製代碼

咱們看直接進入__objc_msgSend_uncached

2.6 __objc_msgSend_uncached探索

STATIC_ENTRY __objc_msgSend_uncached      
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    MethodTableLookup       
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
複製代碼

MethodTableLookup查找

2.7 MethodTableLookup探索

.macro MethodTableLookup
    
    // push frame
    ...省略部分信息...

    // save parameter registers: x0..x8, q0..q7
    ...省略部分信息...

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3  

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ...省略部分信息...
    AuthenticateLR

.endmacro
複製代碼

一頓瘋狂的彙編操做,最後的流程就是到了__class_lookupMethodAndLoadCache3這個函數裏面,這個時候咱們就繼續搜索這麼一個函數,會發現搜索不到它了。

其實到這裏爲止,彙編的快速查找流程已經結束了。下面附上快速查找主線流程圖。

慢速查找流程

1. 查找主線流程

  1. 函數的入口定位

在上面快速查找流程中,沒有緩存命名就會走__class_lookupMethodAndLoadCache3這個函數裏面,那麼它對應在慢速查找流程中的哪一個函數呢,下面咱們經過彙編調試

libobjc.A.dylib`_objc_msgSend_uncached:
    0x100344610 <+0>:   pushq  %rbp
    ...省略部分信息...
->  0x10034464f <+63>:  callq  0x100344ad0   ; ::_class_lookupMethodAndLoadCache3(id, SEL, Class) at objc-runtime-new.mm:5246
...省略部分信息...
複製代碼

經過調試 咱們看到了c++函數_class_lookupMethodAndLoadCache3

加下來咱們開始探索_class_lookupMethodAndLoadCache3

  1. 查看objc源碼
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼
  1. lookUpImpOrForward,這是什麼意思?查找imp或者forward轉發
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }

 retry:    
    runtimeLock.assertLocked();

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class is method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver did not help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}
複製代碼

這一步就是咱們消息慢速查找的流程,接下來對第2步咱們逐步分析。

2. 主線流程深刻

  1. 先看是否有緩存,有就直接返回
if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
複製代碼
  1. runtimeLock.lock()分析這裏加鎖的緣由

防止查找方法A的時候,查找方法B進來,致使返回imp錯誤。

  1. 檢查和準備類
checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }
複製代碼
  • checkIsKnownClass(cls)檢查類是否在內存中存在
  • realizeClass(cls)準備該類的方法,並準備到class_rw_t這個結構體裏面,這裏就不貼相應的代碼了,能夠本身去查看。
  1. 準備就緒,開始從類裏面去查找方法
{
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
}
複製代碼
  • {}這裏括號表明局部做用域;
  • 從當前類裏面找,找到以後cache起來;
  • getMethodNoSuper_nolocklog_and_fill_cache會在下面介紹。
  1. 本身方法裏面沒有找到就開始從父類裏面去查找了。
// Try superclass caches and method lists.
{
  ...省略部分信息...
}
複製代碼

代碼部分省略,和第4步的流程相同,這裏咱們要熟記isa的指向分析與類的繼承關係,會很好的幫助到咱們的理解,在iOS 底層探索篇 —— isa的初始化&指向分析這篇文章裏面已經作了介紹。

到這裏爲止慢速查找的流程已經結束了。如今咱們對第4步中兩個重要的函數分析。

  • getMethodNoSuper_nolock 兩個入參cls sel
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
複製代碼

class_rw_t結構體的methods開始遍歷

  • 分析search_method_list
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

    return nil;
}
複製代碼

這裏分了兩步

  • findMethodInSortedMethodList有序查找,有序查找對應的算法是二分法,一樣也是根據name來判斷。
  • else裏面是針對無序的結構,直接遍歷查找,name相同直接返回。
  • log_and_fill_cache分析
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
複製代碼

SUPPORT_MESSAGE_LOGGING宏定義在咱們iOS的狀況下爲存在的,只是objcMsgLogEnabled這個條件默認爲false,日誌打印就不會開啓。 走向cache_fill,這裏就與iOS 底層探索篇 —— cache_t分析這篇文章的分析銜接上來了。

到這裏咱們的慢速查找流程結束了,下面附上慢速查找主線流程圖。

接下來就要開始forward消息轉發的流程,將在下一篇文章中繼續探索。

相關文章
相關標籤/搜索