手把手帶你探索Runtime底層原理(一)方法查找

Runtime簡單介紹

Objective-C 是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。理解 Objective-C 的 Runtime 機制能夠幫咱們更好的瞭解這個語言,適當的時候還能對語言進行擴展,從系統層面解決項目中的一些設計或技術問題。瞭解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。c++

Runtime 基本是用 C 和彙編寫的,可見蘋果爲了動態系統的高效而做出的努力。你能夠在這裏 密碼:tuw8 下到蘋果維護的開源代碼。蘋果和 GNU 各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。緩存

Runtime消息發送

一個對象的方法像這樣[obj foo],經過 clang -rewrite-objc命令查看編譯後的代碼(因爲以前的文章操做過,這裏不詳細解釋操做流程了),編譯器轉成消息發送objc_msgSend(obj, foo)bash

objc_msgsend 底層有兩種查找方式:架構

  • 快速查找: 經過彙編直接在緩存中找到這個方法並調用
  • 慢速查找: 經過c,c++以及彙編一塊兒完成的

爲何要使用匯編?函數

  1. 彙編經過一個函數保留未知參數,而後跳轉到任意的指針,能夠直接使用寄存器保存,而C沒法實現
  2. 彙編代碼執行的效率高,執行的時間週期準確

快速查找

先在剛剛提供的objc源碼裏查找objc_msgSend,找到咱們經常使用的架構arm64彙編文件,能夠看到ENTRY _objc_msgSend,其實就是這個函數的入口 oop

1._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		// calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	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 _objc_msgSend
複製代碼

可能看不懂彙編,根據註釋大概推測其意思,下面的代碼主要作了非空檢查和無標記指針檢查(若是指針小於等於 LNilOrTagged 直接return返回)post

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

接下來看以下代碼,根據isa獲取這個類,LGetIsaDone是表示isa處理完畢,CacheLookup NORMAL表示直接調用當前的imp或者發送objc_msgSend_uncached無緩存消息ui

ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
複製代碼

2. CacheLookup宏定義

在當前文件裏搜索CacheLookup,來到它的宏定義this

.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宏定義這裏spa

  • CacheHit: 緩存命中,方法的實現IMP在寄存器中,而後傳遞出去
  • CheckMiss: 緩存沒命中,發送_objc_msgSend_uncached
  • add: 若是緩存裏沒找到,去其餘地方查找到該方法實現後添加到緩存

3. CacheHit 和 CheckMiss

.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	AuthAndResignAsIMP x0, x12	// authenticate imp and re-sign as IMP
	ret				// return IMP
.elseif $0 == LOOKUP
	AuthAndResignAsIMP x17, x12	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
複製代碼

看到CacheHit的宏定義: 在上面調用的時候,傳遞過來的是NORMAL,執行了TailCallCachedImp,即若是緩存命中的話,則返回緩存裏的IMP.

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

再看到CheckMiss的宏定義:在上面調用的是NORMAL,因此這裏會發送__objc_msgSend_uncached的消息

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

發現這裏調用了MethodTableLookup,因此繼續跟進查看

.macro MethodTableLookup
	
	// 省略
	// 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
	//省略

.endmacro
複製代碼
  • 前面後面的彙編代碼只能看到有作字節對齊的操做,不過因爲不懂彙編,具體作什麼不是很清楚,不過看到了跳轉進了一個很是重要的函數__class_lookupMethodAndLoadCache3

  • 繼續搜索__class_lookupMethodAndLoadCache3發現並不能在當前彙編文件裏找到聲明,這時猜測會不會是跳轉到了代碼裏

  • 因而全局搜索class_lookupMethodAndLoadCache3,果真在objc-runtime-new.mm文件裏找到了它的函數實現

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

因此這裏經過調用lookUpImpOrForward開啓了慢速查找的過程

快速查找總結

慢速查找

來到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.lock();
    checkIsKnownClass(cls);

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

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
    //先省略
}
複製代碼
  • 首先分析一下從傳遞進來的三個參數,根據註釋 initialize爲YES,cache爲NO,resolver爲YES
  • 首先解釋一下爲何彙編傳遞的三個參數爲這幾個值?在上面分析的彙編裏,LGetIsaDone這個判斷是在isa處理完畢後才走緩存查找的彙編代碼的,因此這個類是加載解析好的,即initializeresolver都爲YES,cache爲NO是由於在彙編裏快速查找沒有找到方法緩存纔會執行到這裏,因此這裏確定爲NO
  • 接下來看到這裏判斷是否有緩存,若是有直接調用匯編裏的cache_getImp去獲取imp,因爲傳遞進來的cache爲NO,因此這裏不會執行
// Optimistic cache lookup
    // 查找緩存!!!
    if (cache) {
        //彙編代碼的方式實現的!
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    
    //彙編代碼(在以前上面的彙編文件裏)
    STATIC_ENTRY _cache_getImp
    GetClassFromIsa_p16 p0
	 CacheLookup GETIMP
複製代碼
  • 繼續看下去checkIsKnownClass(cls);檢查這個類是否已知,若是未知則拋出異常
  • 接下來判斷類有沒有實現和有沒有初始化,沒有則調用實現方法realizeClass(cls)和初始化方法_class_initialize (_class_getNonMetaClass(cls, inst));

慢速查找重點:retry

lookUpImpOrForward裏還有retry相關的代碼,繼續分析

retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.

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

    // Try this class's method lists.
    {
        //Method(SEL IMP)
        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.
    //_objc_msgForward
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
複製代碼
  1. 第一步看到這裏再一次調用了cache_getImp(cls, sel);去從緩存中獲取imp,傳遞進來明明已經知道是NO了,爲何再去查找一次呢?

  2. objc_init的時候有一個函數remap(cls),在彙編最開始查找該方法的時候若是沒有方法緩存,但可能會在這個類初始化方法objc_init的過程當中,對這個類進行了重映射remap,即把該方法添加到方法緩存裏了,因此這裏要再去查找一次cache有緩存就可能會節省不少時間

  3. 接下來先從當前類的方法列表method_list去找,找到了就log_and_fill_cache打印日誌並把方法添加到緩存中

  4. 若是沒找到則繼續找父類的緩存cache_getImp(curClass, sel),再找父類的方法列表,和以前在本類的查找順序同樣,找到了也是添加到方法緩存log_and_fill_cache

  1. 慢速查找到上面已經結束了,因爲上面整個過程比較慢,因此通常稱爲慢速。若是沒有找到imp,則進行動態方法解析和消息轉發,篇幅緣由,接下來的這個過程在Runtime底層原理(二)動態方法解析和消息轉發

慢速查找總結

以上均爲我的探索源碼的理解和所得,若有錯誤請指正,歡迎討論。

相關文章
相關標籤/搜索