OC源碼分析之方法的查找原理

img_cover@1x.jpg

前言

想要成為一名iOS開發(fā)高手,免不了閱讀源碼。以下是筆者在OC源碼探索中梳理的一個小系列——類與對象篇,歡迎大家閱讀指正,同時也希望對大家有所幫助。

  1. OC源碼分析之對象的創(chuàng)建
  2. OC源碼分析之isa
  3. OC源碼分析之類的結(jié)構(gòu)解讀
  4. OC源碼分析之方法的緩存原理
  5. OC源碼分析之方法的查找原理
  6. OC源碼分析之方法的解析與轉(zhuǎn)發(fā)原理

Objective-C中,當(dāng)編譯器遇到一個方法調(diào)用時,它會將方法的調(diào)用變成以下函數(shù)中的一個:

objc_msgSendobjc_msgSend_stret、objc_msgSendSuperobjc_msgSendSuper_stret

發(fā)送給對象的父類的消息(使用super關(guān)鍵字時)是使用objc_msgSendSuper發(fā)送的,其他消息是使用objc_msgSend發(fā)送的。如果是以數(shù)據(jù)結(jié)構(gòu)體作為返回值的方法,則是使用objc_msgSendSuper_stretobjc_msgSend_stret發(fā)送的。

上面四個函數(shù)都用于發(fā)送消息,做一些準(zhǔn)備工作,繼而進(jìn)行方法的查找、解析和轉(zhuǎn)發(fā)。本文的主題是方法的查找,筆者將從方法調(diào)用開始,一步一步詳細(xì)解讀objc_msgSend函數(shù)的實(shí)現(xiàn)以及方法的查找流程。

下面直接進(jìn)入正題。

需要注意的是,筆者用的源碼是 objc4-756.2。

1 objc_msgSend解析

1.1 舉個栗子

嗯,一個簡單的例子,代碼如下

@interface Person : NSObject

- (void)personInstanceMethod1;

@end

@implementation Person

- (void)personInstanceMethod1 {
    NSLog(@"%s", __FUNCTION__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        [person personInstanceMethod1];
    }
    return 0;
}

clang命令重新編譯main.m文件

clang -rewrite-objc main.m -o main.cpp

打開main.cpp文件

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        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("personInstanceMethod1"));
    }
    return 0;
}

去除強(qiáng)轉(zhuǎn)后就容易分辨多了

Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("personInstanceMethod1"));

發(fā)現(xiàn)+allocpersonInstanceMethod1這兩個方法的調(diào)用,實(shí)際上就是調(diào)用objc_msgSend函數(shù)。

1.2 方法調(diào)用的本質(zhì)

可以在arm64.s文件中找到objc_msgSend函數(shù)的說明

id objc_msgSend(id self, SEL _cmd, ...)

其中,第一個參數(shù)self是調(diào)用者本身,它也是接收者;第二個參數(shù)_cmd是方法編號;剩下的可變參數(shù)列表是方法自己的參數(shù)。

簡單說明一下:

  • id指的是OC對象,每個對象在內(nèi)存的結(jié)構(gòu)都是不確定的,但其首地址指向的是對象的isa,通過isa,在運(yùn)行時就能獲取到objc_class
  • objc_class表示對象的Class,它的結(jié)構(gòu)在編譯后就確定了
  • SEL表示選擇器,通??衫斫鉃橐粋€字符串。OC在運(yùn)行時維護(hù)著一張SEL表,將字符串相同的方法名映射到唯一一個SEL
    • 任意類的相同方法名映射的SEL都相同(可以把SEL近似地等同于方法名)
    • 可以通過sel_registerName(char *name)這個C函數(shù)得到SEL,OC也提供了一個語法糖@selector用來方便的調(diào)用該函數(shù)
  • IMP是一個函數(shù)指針。OC中的方法最終都會轉(zhuǎn)換成純C的函數(shù),IMP表示的就是這些函數(shù)的地址。

從上面的例子可以得出一個結(jié)論:方法調(diào)用的本質(zhì)是通過objc_msgSend函數(shù),向調(diào)用者發(fā)送名為SEL的消息,找到具體的函數(shù)地址IMP,進(jìn)而執(zhí)行該函數(shù)。

也就是說,下面的兩段代碼實(shí)際上效果是相同的

image
要想使用 objc_msgSend 函數(shù),需要改一處設(shè)置。如下圖
image

2 方法的查找

方法的查找,也叫消息的查找,它的準(zhǔn)備工作是從objc_msgSend開始,準(zhǔn)備就緒后,才會展開查找。

2.1 objc_msgSend源碼分析

arm64架構(gòu)為例,objc_msgSend的源碼以及解析如下

    // ENTRY 表示函數(shù)入口
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    // p0存儲的是objc_msgSend的第一個參數(shù)(即接收者)
    // 在此對接收者進(jìn)行非空判斷
    cmp p0, #0              // nil check and tagged pointer check
    // 是否支持 Tagged Pointer,64位CPU架構(gòu)下為1
#if SUPPORT_TAGGED_POINTERS 
    // 64位,且 p0 <= 0(le即less or equal),跳轉(zhuǎn)到 LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    // 32位,且 p0 == 0(eq即equal),跳轉(zhuǎn)到 LReturnZero
    b.eq    LReturnZero
#endif
    // 讀取接收者(實(shí)例對象、類對象、元類對象)的isa到p13
    ldr p13, [x0]               // p13 = isa
    // 根據(jù)isa得到class
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    // p0 == 0,即接收者為nil,跳轉(zhuǎn)到 LReturnZero
    b.eq    LReturnZero         // nil check

    // 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 表示函數(shù)結(jié)束
    END_ENTRY _objc_msgSend

可以看出,objc_msgSend主要是獲取接收者的isa。

思考:objc_msgSend為什么要用匯編編寫?

2.2 GetClassFromIsa_p16

以下兩種情況下會執(zhí)行GetClassFromIsa_p16

  • 當(dāng)系統(tǒng)是64位架構(gòu),接收者不是Tagged Pointer對象,isa也不是nonpointer的;
  • 當(dāng)系統(tǒng)不是64位架構(gòu),且接收者的isa非空

以上兩種情況下會來到GetClassFromIsa_p16,其源碼如下

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA     // armv7k or arm64_32
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

由上文可知p16 = class,$0是接收者isa,64位架構(gòu)下通過isa & ISA_MASK得到真正的isa,其值是類或元類,這取決于接收者是實(shí)例對象還是類對象。

也就是說,objc_msgSend的主要作用是拿到接收者的isa信息,如果有,則執(zhí)行CacheLookup

LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

2.3 CacheLookup

經(jīng)過objc_msgSend一番操作,此時p1 = SEL,p16 = isa,

CacheLookup的作用就是在緩存中查找方法實(shí)現(xiàn),它有三種模式:NORMAL、GETIMPLOOKUP。

先來看看CacheLookup源碼

.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

CacheLookup操作的是類的cache成員變量,它的結(jié)構(gòu)是cache_t,主要用于緩存調(diào)用的方法(想了解cache_t的請戳 OC源碼分析之方法的緩存原理,這里不作贅述)。

CacheLookup的分析如下:

  1. 關(guān)于ldp p10, p11, [x16, #CACHE]

找到CACHE的定義處

#define CACHE            (2 * __SIZEOF_POINTER__)
#define CLASS            __SIZEOF_POINTER__

64位CPU架構(gòu)下,指針的長度為8字節(jié),所以CACHE為16字節(jié)。對于cache_t結(jié)構(gòu),其中buckets指針為8字節(jié),存放在p10;4字節(jié)的mask和4字節(jié)的occupied共同存放在p11,p11的低32位(即w11)存放的是mask。

  1. 找到目標(biāo)bucket
#if __LP64__    // arm64
...
#define PTRSHIFT 3
...

#else           // arm64_32
...
#define PTRSHIFT 2
...

通過sel & mask哈希計(jì)算得出索引值,再取到對應(yīng)的bucket,接著將bucketimpsel分別存入p17、p9。

思考:為什么索引值要左移1 + PTRSHIFT位?

  1. CacheHit、CheckMissJumpMiss

當(dāng)找到bucket后,接下來的流程如下:

  • 如果bucketsel不等于方法的sel,則執(zhí)行{imp, sel} = *--bucket,也就是遍歷buckets中的每個bucket,分別與方法的sel作對比
  • 如果bucketsel等于方法的sel,則執(zhí)行CacheHit,即直接返回并執(zhí)行imp;
  • 如果找到buckets的第一個bucket,則執(zhí)行JumpMiss
  • 如果bucketsel等于0,即該bucket是空桶,則執(zhí)行CheckMiss

接下來看一下CacheHitCheckMissJumpMiss這三個函數(shù)的源碼

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

// CheckMiss
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

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

關(guān)于這三個函數(shù),總結(jié)如下:

  • CacheHit函數(shù)在NORMAL模式下,會把找到的IMP返回并調(diào)用;在GETIMP、LOOKUP這兩種模式下僅僅是返回IMP,并沒有調(diào)用。
  • JumpMiss、CheckMiss這兩個函數(shù)在三種模式下的行為基本一致:
    • NORMAL模式下,均調(diào)用__objc_msgSend_uncached;
    • GETIMP模式下,均調(diào)用LGetImpMiss,返回nil
    • LOOKUP模式下,均調(diào)用__objc_msgLookup_uncached;

2.4 __objc_msgSend_uncached__objc_msgLookup_uncached

同樣看源碼

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

// __objc_msgLookup_uncached

STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

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

END_ENTRY __objc_msgLookup_uncached

發(fā)現(xiàn)這兩個函數(shù)的主要工作就是調(diào)用MethodTableLookup

2.5 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)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __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
    AuthenticateLR

.endmacro

MethodTableLookup函數(shù)主要保存部分寄存器的參數(shù),然后就是調(diào)用_class_lookupMethodAndLoadCache3函數(shù)。來到這里,就意味著消息的緩存查找流程正式結(jié)束,接下來就要去方法列表中查找了。在方法列表中的查找流程是C\C++實(shí)現(xiàn)的,效率低于緩存查找,因此這個流程也叫做消息的慢速查找流程。

2.6 _class_lookupMethodAndLoadCache3

_class_lookupMethodAndLoadCache3是個簡單的C\C++函數(shù),它只有一行代碼,即調(diào)用lookUpImpOrForward函數(shù)

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

接下來重點(diǎn)分析lookUpImpOrForward函數(shù)。

2.7 lookUpImpOrForward

既然是從_class_lookupMethodAndLoadCache3過來的,顯然initializeresolver這兩個參數(shù)的值是YESresolver這個標(biāo)志決定是否進(jìn)行后面的動態(tài)方法解析),cache則是NO。下面分析一下lookUpImpOrForward的源碼

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
    // 如果是從緩存過來,cache為NO;消息解析和轉(zhuǎn)發(fā)的時候,需要過一遍緩存,此時為YES;
    // 對緩存進(jìn)行查找是沒有加鎖的,進(jìn)而提高緩存查找的性能
    if (cache) {
        // cache_getImp 也是匯編編寫的
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.
    // 加鎖,防止多線程操作,保證方法查找以及緩存填充(cache-fill)的原子性,
    // 以及確保加鎖之后的代碼不會有新方法添加導(dǎo)致緩存被沖洗(flush).
    runtimeLock.lock();
    checkIsKnownClass(cls);
    // 如果類還沒有realize,需要先進(jìn)行realize,加載信息(屬性、方法、協(xié)議等的attach)
    // 一般懶加載的類會走此方法
    if (!cls->isRealized()) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    // 如果類未初始化,需要初始化
    if (initialize && !cls->isInitialized()) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }


 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
    // 在當(dāng)前類的緩存中查找
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    // 在當(dāng)前類的方法列表中查找
    {
        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 {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    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.
    // 在【類...根類】的【緩存+方法列表】中都沒找到IMP,進(jìn)行方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    // 消息的轉(zhuǎn)發(fā)
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

簡單梳理一下。對于某個cls來說,查找IMP總是會先從cls的緩存中開始(調(diào)用cache_getImp函數(shù));如果沒找到,才會去cls的方法列表中查找,也就是調(diào)用getMethodNoSuper_nolock函數(shù)。如果依然沒找到,會去cls的父類(父類的父類,一直到根類)重復(fù)【緩存+方法列表】這個查找流程,直到找到為止。如果到根類后依然未找到,則會進(jìn)入方法解析甚至消息轉(zhuǎn)發(fā)的流程。

需要注意的是,如果在當(dāng)前類的緩存中沒找到,但是在其方法列表(或其“父類...根類”的緩存或方法列表)中找到了IMP,需要進(jìn)行一次是否是消息轉(zhuǎn)發(fā)的判斷,如果不是消息轉(zhuǎn)發(fā),那么就對當(dāng)前類的緩存進(jìn)行填充操作,方便下次的調(diào)用;如果是消息轉(zhuǎn)發(fā),就退出循環(huán)。

lookUpImpOrForward 這個函數(shù)可以說是消息的調(diào)度中心,它不僅包含消息的查找,還囊括了消息的解析和轉(zhuǎn)發(fā)。礙于篇幅,本文僅介紹其消息查找方面的內(nèi)容,其余內(nèi)容將另啟一文詳細(xì)說明。

接下來分析一下cache_getImpgetMethodNoSuper_nolock這兩個函數(shù)。

2.8 cache_getImp

cache_getImp也是匯編編寫的,其源碼如下:

STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp

主要是處理isa,進(jìn)而在緩存中查找IMP,需要注意的是,這次的緩存查找模式是GETIMPGETIMP模式下,如果CacheLookup查找失敗會執(zhí)行LGetImpMiss)。

關(guān)于GetClassFromIsa_p16CacheLookup前面已做過解析。

2.9 getMethodNoSuper_nolock

調(diào)用getMethodNoSuper_nolock函數(shù)的目的是檢索cls的方法列表,查找名為sel的方法,找到則返回這個方法(method_t結(jié)構(gòu),該結(jié)構(gòu)含有IMP)。其源碼為:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil 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;
}

cls->data()->methodsmethod_array_t結(jié)構(gòu),它可能是一維數(shù)組,也可能是二維數(shù)組,對其從beginLists()endLists()進(jìn)行迭代,確保了每次迭代時總能得到一維的method_t數(shù)組,接下來就是調(diào)用search_method_list函數(shù),對這個數(shù)組進(jìn)行檢索。

2.10 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)) {
        // 一般來說,mlist是有序的,由此對其進(jìn)行二分查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        // mlist是無序的,只好遍歷匹配
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

__builtin_expect()表示對結(jié)果的期望:多數(shù)都是true,也就是說,這里通常會執(zhí)行findMethodInSortedMethodList函數(shù),對mlist進(jìn)行二分查找;否則將遍歷查找。

需要注意的是,當(dāng)方法列表的結(jié)構(gòu)發(fā)生改變的時候,就會觸發(fā)對列表的排序(注意是方法所在的列表,而不是rw中所有的方法列表,即如果是二維數(shù)組,只需要重新排列當(dāng)前方法所在的列表即可)。在以下函數(shù)的調(diào)用中,會觸發(fā)對mlist的排序:

  • methodizeClass
  • attachCategories
  • addMethod
  • addMethods

2.11 log_and_fill_cache

log_and_fill_cache函數(shù)主要是將找到的IMP填充到緩存中,方便下次的調(diào)用。下面的三種情況會調(diào)用這個函數(shù):

  • 在類(或轉(zhuǎn)發(fā)類)的緩存中沒找到IMP,但是在其方法列表中找到了;
  • 在類(該類不是轉(zhuǎn)發(fā)類)的“父類...根類”的緩存中找到了IMP;
  • 在類的“父類...根類”的方法列表中找到了IMP
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);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif

log_and_fill_cache函數(shù)比較簡單,它調(diào)用了cache_fill函數(shù),而cache_fill函數(shù)又調(diào)用了cache_fill_nolock函數(shù),也就是說,填充緩存的關(guān)鍵函數(shù)是cache_fill_nolock。

關(guān)于cache_fill_nolock函數(shù)的解析,感興趣的同學(xué)請戳 OC源碼分析之方法的緩存原理,筆者有詳細(xì)分析,這里就不作贅述。

3 總結(jié)

來到這里,已經(jīng)把消息的查找介紹完畢,是時候總結(jié)一下了。

3.1 方法調(diào)用的本質(zhì)

  • 方法的調(diào)用會被編譯器翻譯成 objc_msgSend、objc_msgSendSuper、objc_msgSend_stretobjc_msgSendSuper_stret 這四個函數(shù)之一,這四個函數(shù)都是由匯編代碼實(shí)現(xiàn)的。
    • 如果是以數(shù)據(jù)結(jié)構(gòu)體作為返回值的方法,最終會轉(zhuǎn)換成相應(yīng)的objc_msgSend_stretobjc_msgSendSuper_stret函數(shù)
    • 使用super關(guān)鍵字調(diào)用方法時,是使用objc_msgSendSuper發(fā)送的
    • 其他的方法調(diào)用是使用objc_msgSend函數(shù)發(fā)送消息的。
  • 方法的調(diào)用是通過objc_msgSend(或objc_msgSendSuper,或objc_msgSend_stret,或objc_msgSendSuper_stret)函數(shù),向調(diào)用者發(fā)送名為SEL的消息,找到具體的函數(shù)地址IMP,進(jìn)而執(zhí)行該函數(shù)。

3.2 方法的查找

方法的查找流程如下:

  1. objc_msgSend源碼開始,會先去 類(實(shí)例方法)\元類(類方法) 的緩存中查找,如果找到IMP則返回并調(diào)用,否則會去 類\元類 的方法列表中查找
  2. “步驟1”中的 “緩存+方法列表” 的查找方案,會遍歷類的繼承體系(類、類的父類、...、根類),分別進(jìn)行查找,直至找到IMP為止。
    • 如果在當(dāng)前類的緩存中沒找到,但是在其方法列表(或其“父類...根類”的緩存或方法列表)中找到了IMP,需要進(jìn)行一次是否是消息轉(zhuǎn)發(fā)的判斷,如果不是消息轉(zhuǎn)發(fā),那么就對當(dāng)前類的緩存進(jìn)行填充操作,方便下次調(diào)用時的查找;如果是消息轉(zhuǎn)發(fā),則不會緩存到當(dāng)前類中
  3. 如果遍歷結(jié)束后依然未找到IMP,則會啟動消息的解析或轉(zhuǎn)發(fā)。

4 問題討論

4.1 objc_msgSend為什么要用匯編編寫?

A:原因大致有以下幾點(diǎn)

  • C語言是靜態(tài)語言,無法實(shí)現(xiàn)參數(shù)個數(shù)、類型未知的情況下跳轉(zhuǎn)到另一個任意的函數(shù)實(shí)現(xiàn)的功能;而匯編的寄存器可以做到這一點(diǎn)
  • 匯編執(zhí)行效率比C語言的高
  • 使用匯編可以有效防止系統(tǒng)函數(shù)被hook,因此更為安全。

4.2 為什么索引值要左移1 + PTRSHIFT位?

A:這個筆者沒有在objc4-756.2源碼中找到答案,但是在objc4-779.1源碼版本的cache_t結(jié)構(gòu)中,存在關(guān)于這個問題的解釋。其部分源碼如下:

// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
    
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
    
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;

總的來說,在64位中,mask的偏移值為48,也就是最高的16位存儲mask;接著的44位是buckets指針地址,最低的4位是附加位,即 buckets的有效指針地址僅僅是64位中的[4, 47]位。在CacheLookup源碼中,由_cmd & mask哈希運(yùn)算可以得到索引值(索引值 < ((1 << 16) - 1)),如果想得到這個位置的bucket,其索引值必須左移4位后,才能與buckets指針地址相加得到正確的bucket地址。

5 參考資料

6 PS

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容