iOS的OC的方法的查找原理

前言

筆者整理了一系列有關OC的底層文章,但願能夠幫助到你。這篇文章主要講解的是方法查找原理分析c++

1.iOS的OC對象建立的alloc原理緩存

2.iOS的OC對象的內存對齊性能優化

3.iOS的OC的isa的底層原理bash

4.iOS的OC源碼分析之類的結構分析架構

5.iOS的OC的方法緩存的源碼分析併發

iOS的開發中咱們會使用類中的各類方法,在OC中對方法的調用稱爲消息的發送。對方法函數的使用每個iOS開發者都很熟悉的,可是方法函數是怎麼在底層中是怎麼查找的就是這篇文章主要來介紹的。app

1. 方法的本質

爲了方便介紹接下來的內容,建立一個macOS的項目,定義了一個TestObject的類定義了一個testMethod的方法,在main.m的文件裏面實現以下代碼函數

#import <Foundation/Foundation.h>
#import "TestObject.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestObject *objc = [[TestObject alloc] init];
        [objc testMethod];
    }
    return 0;
}
複製代碼

而後在該項目的目錄下用終端命令clang -rewrite-objc main.m直接編譯生成一個main.cpp文件來查看上面代碼的底層實現,最終獲得代碼以下oop

爲了簡化代碼,去掉一些轉換,最終獲得

TestObject *objc = objc_msgSend(objc_getClass("TestObject"), sel_registerName("alloc")), sel_registerName("init"));
objc_msgSend(objc, sel_registerName("testMethod"));
複製代碼

其中sel_registerName函數至關於@selector,在TestObject類調用alloc,inittestMethod等方法都是在底層經過objc_msgSend來進行發送消息的,能夠看出方法的本質就是經過objc_msgSend來發送消息的。其中objc_msgSend有兩個參數,id是消息的接收者,SEL方法的編號。其中經過以前的objc4-756.2的源碼查找到能夠知道,方法的調用在底層會分別被編譯成objc_msgSend, objc_msgSend_stret, objc_msgSendSuperobjc_msgSendSuper_stret。若是調用父類的方法會編譯成帶有super字段的函數,其中objc_msgSend_stret是調用結構體的方法。源碼分析

2.方法的快速查找

經過objc4-756.2的源碼找到objc_msgSend的底層源碼是經過彙編的方式來寫的,接下來的源碼介紹是在arm64的架構下的。

2.1 objc_msgSend彙編

//objc_msgSend函數的入口
	ENTRY _objc_msgSend
    //objc_msgSend沒有窗口
	UNWIND _objc_msgSend, NoFrame
    //對比當前的p0第一位是否爲空或者是taggedPointer,若是是nil會跑到LReturnZero中,
    //若是是taggedPointer會跑到LNilOrTagged
	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
    //在正常的狀況下不是nil不是taggedPointer,會執行到這裏
    //其中p13爲isa,若是消息的接收者是對象經過isa能夠找到類,若是是類能夠找到元類
	ldr	p13, [x0]		// p13 = isa
    //這裏就去到GetClassFromIsa_p16的宏方法,將p13爲isa做爲參數
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
複製代碼

從中能夠看到,進入到objc_msgSend彙編裏面會先判斷傳進來的接收者是否爲空和是不是taggedpointer,若是都不是就先找到isa,經過isa找到class,接下來介紹GetClassFromIsa_p16

2.2 GetClassFromIsa_p16

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
	// 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
複製代碼

這部分的源碼在arm64架構下只會走#elif __LP64__下的,經過傳進來的isa&ISA_MASK獲得class,而且以p16返回,最終仍是返回上面的objc_msgSend外面的,會繼續執行LGetIsaDoneCacheLookup

2.3 CacheLookup

其中CacheLookup有三種查找的方式CacheLookup NORMAL|GETIMP|LOOKUP,NORMAL是正常的流程,GETIMP查找impLOOKUP方法的查找。

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

// CacheHit: x17 = cached IMP, x12 = address of cached IMP

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

//這是在緩存cache_t中查找方法
.macro CacheLookup
    //其中x16是找到的class,經過#CACHE獲得16個字節,從而class右移16字節獲得cache_t
    //其中cache_t是一個結構體,佔16字節,bucket_t佔8個字節,mask和occupied分別佔4個字節
    //並將cache_t中的buckets賦值給p10,occupied和mask賦值給p11
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
    //x12是獲得的hash值
	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
複製代碼

這部分的內容就是查找到cache_t,並在cache_t查找傳進來的方法是否在這裏,具體的cache_t的方法緩存能夠看iOS的OC的方法緩存的源碼分析這篇文章的介紹。在1部分的內容判斷buckectsel與傳進來的cmd是否相等,便是否有緩存過的方法,若是緩存命中CacheHit直接返回imp,若是沒有緩存的就去到2部分的內容。執行CheckMiss

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

這部分是根據以前的傳進來的$0參數來判斷須要執行那一部分。由上面的內容可知傳進來的$0NORMAL。接下來的執行__objc_msgSend_uncached,至此objc_msgSend經過cache_t來快速查找部分就結束了,接下來的部分就是經過慢速的方法查找。

3.方法的慢速查找

objc_msgSend經過第2部分的cache_t快速查找,在緩存中找不到有緩存的方法,此時就須要進行沒有緩存的慢速查找。

3.1 objc_msgSend_uncached和MethodTableLookup

這部分的內容就是

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

經過以前的文章iOS的OC源碼分析之類的結構分析能夠知道方法的是存類的bitsrorw裏面的methodList的,在cache_t裏面找不到方法的時候,此時就須要在methodList找了,而MethodTableLookup就是爲了這部份內容作的準備。最終會執行到__class_lookupMethodAndLoadCache3這個函數。

3.2 class_lookupMethodAndLoadCache3

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

傳進來的obj是對象,sel是方法的編號,cls是類。而後直接調用lookUpImpOrForward函數,此時進來的參數中initialize是YES,cache是NO,resolver是YES,由於此時是在cache_t緩存中找不到方法才執行到這裏的。

3.3 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
    if (cache) {
        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.

    runtimeLock.lock();
    checkIsKnownClass(cls);

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

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // 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.

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

    // Try this class's 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 { // 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.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_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. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlock(); return imp; } 複製代碼

這個lookUpImpOrForward函數的代碼有點多,就分開一點點地分析。其中runtimeLock是防止線程併發競爭的鎖。

if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
複製代碼

這裏是再次判斷若是有緩存的,直接在緩存中找到imp返回出去。

runtimeLock.lock();
    checkIsKnownClass(cls);

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

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // 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 } 複製代碼

上面這部分的內容是判斷類是不是合法的,而且判斷類是不是初始化了,若是沒有初始化好的話,就須要進入到realizeClass函數裏面進行初始化,這個函數也是對當前的類的父類和元類都作了初始化,這部分的內容就是爲了接下來的類在bits裏面查找方法作好準備的。

// Try this class's 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; } } 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; } 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); } 複製代碼

這部分的內容是在類class裏面查找方法。getMethodNoSuper_nolock函數是在類cls的data()裏面的methodList列表循環查找sel。若是找到就返回method_t。而且執行log_and_fill_cache函數,到最後仍是會執行cache_fill。此時會將方法再次緩存在cache_t中。

// 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; } } } 複製代碼

這部分的內容是在類裏面查找不到方法了,須要去父類查找方法。由於咱們以前的查找都是對當前的類開啓objc_msgSend彙編查找和cls的bits的methodList查找的,父類的方法也是可能有緩存的,因此此時經過父類的循環首先是經過cache_getImp函數來查找imp。其中_objc_msgForward_impcache是實際存儲在其中的函數指針方法緩存。若是有直接執行log_and_fill_cache函數直接done,若是沒有就break出去。若是沒有找到imp或者找到imp作轉發了此時不緩存,會直接調用getMethodNoSuper_nolock函數來查找。若是找到仍是會對這個方法作緩存的。

4.方法查找失敗

上面的介紹都是方法存在的,若是在方法查找的過程當中,查找不到的話是會報異常的,例如執行以下的代碼

TestObject *testObject = [[TestObject alloc] init];
[testObject performSelector:@selector(testErrorMthod)];
複製代碼

經過上面的源碼能夠知道,當方法在慢速查找和快速查找的過程當中都找不到,而且也不作消息轉發的處理,最終會執行到以下的源碼

// No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_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. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); 複製代碼

最終會執行_objc_msgForward_impcache函數,而_objc_msgForward_impcache是彙編的

STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
複製代碼

由此可知,__objc_msgForward_impcache會執行到__objc_msgForward,最終會執行到__objc_forward_handler。經過源碼的查找,最後會執行objc_defaultForwardHandler函數打印出錯誤的信息。

#if !__OBJC2__

// Default forward handler (nil) goes to forward:: dispatch.
void *_objc_forward_handler = nil;
void *_objc_forward_stret_handler = nil;

#else

// Default forward handler halts the process.
__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret 
objc_defaultForwardStretHandler(id self, SEL sel)
{
    objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif
複製代碼

爲何objc_msgSend的底層是用匯編來寫的呢?

  • 爲了更好地性能優化和更加容易被機器識別,由於在項目中方法的調用是很是頻繁的也是很消耗性能的,因此用匯編能夠更加地優化性能。
  • 有時有的方法的調用會有一些未知的參數和未知的類型,若是用c或者c++都是很難實現的,彙編能夠很完美地解決這些問題。

5 總結

方法的查找是在objc_msgSend的函數下進行的,這一個過程有快速查找和慢速查找。

  • 快速查找:先經過objc_msgSend快速查找,而objc_msgSend是在彙編的狀況下進行的。進入objc_msgSend先判斷第一位的內存值是否爲空或者是taggedPointer,若是是就走相應的流程。若是不是就是正常的流程就須要經過GetClassFromIsa_p16找到isa,經過isa執行CacheLookup去到類的cache_t來查找是否緩存方法,若是沒有就執行__objc_msgSend_uncached。此時就至關於快速查找方法是找不到了,須要過分到慢速的查找。
  • 慢速查找:經過__objc_msgSend_uncached能夠執行MethodTableLookup函數來爲接下來須要在類的bits中查找的方法做準備。最終會在彙編中過渡到c++函數,執行class_lookupMethodAndLoadCache3。經過lookUpImpOrForward函數來分別遍歷類和父類的方法列表中查找,若是找到就緩存在cache_t中。若是沒有找到,而且沒有作消息轉發的操做,最終會執行_objc_msgForward_impcache而後進去__objc_msgForward__objc_forward_handler函數報錯。

至此,方法的查找底層原理就介紹完畢了。

相關文章
相關標籤/搜索