爲何要設計metaclass

原文連接面試

今早起牀打開微信,發現知識小集推送了一篇文章《阿里、字節:一套高效的iOS面試題》,打開瞅了眼,看到第二題就給我看懵圈了,爲何要設計metaclass?在個人知識體系中關於元類的認知是類對象的isa指向元類對象,元類對象存儲着類方法列表,而後就沒有而後了。數組

帶着這個疑問我邊開始google了,找到一文Why is MetaClass in Objective-C?,該文很好的解釋了OC面向對象能力的部分師承於Smalltalk,經過類的劃分和消息傳遞兩個亮點解釋了爲何要有metaclass,可是我想僅僅經過設計層面解釋恐怕打動不了面試官,若是面試官反問爲何OC要借鑑Smalltalk這門語言呢?畢竟咱對Smalltalk也不瞭解。緩存

OK,既然元類的存在跟方法有關,那麼咱們就從方法的調用階段入手。bash

源代碼來自objc-750微信

__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

複製代碼

源碼中給了部分註釋,不肯看代碼的直接看下面的流程吧 一、進入_objc_msgSend後首先判斷消息的接受者是否爲nil或者是否使用了tagPointer技術,因爲本文是爲了探究META-CLASS存在的意義,因此關於tagPointer的東西就直接忽略了。 二、根據消息接受者的isa指針找到metaclass(由於類方法存在元類中。若是調用的是實例方法,isa指針指向的是類對象。) 三、進入CacheLookup流程,這一步會去尋找方法緩存,若是緩存命中則直接調用方法的實現,若是緩存不存在則進入objc_msgSend_uncached流程。oop

CacheLookup

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

.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

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

以前的_objc_msgSend代碼中咱們知道CacheLookup走的是NORMAL流程,別的支線代碼就忽略了 從上述代碼中可見得知當緩存命中時會調用TailCallCachedImp驗證方法IMP的有效性並調用改方法的實現,若是緩存沒有命中則進入__objc_msgSend_uncached流程。ui

關於緩存是如何緩存和尋找緩存的,後續會寫篇blog進行詳解。google

__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
	
    *這裏忽略寄存器的操做*

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

	*這裏忽略寄存器的操做*

.endmacro

複製代碼

一通操做後從後面調用到了_class_lookupMethodAndLoadCache3這個方法,該方法在objc_runtim_new.mm文件中,終於從彙編代碼中走了出來!spa

__class_lookupMethodAndLoadCache3

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

複製代碼

該方法會去調用lookUpImpOrForward,因爲lookUpImpOrForward方法篇幅有點長,這裏簡述一下該方法的流程。debug

一、首先會再一次的從類中尋找須要調用方法的緩存,若是能命中緩存直接返回該方法的實現,若是不能命中則繼續往下走。

二、從類的方法列表中尋找該方法,若是能從列表中找到方法則對方法進行緩存並返回該方法的實現,若是找不到該方法則繼續往下走。

三、從父類的緩存尋找該方法,若是父類緩存能命中則將方法緩存至當前調用方法的類中(注意這裏不是存進父類),若是緩存未命中則遍歷父類的方法列表,以後操做如同第2步,未能命中則繼續走第3步直到尋找到基類。

四、若是到基類依然沒有找到該方法則觸發動態方法解析流程。

五、仍是找不到就觸發消息轉發流程

走到這裏一套方法發送的流程就都走完了,那這跟元類的存在有啥關係?咱們都知道類方法是存儲在元類中的,那麼可不能夠把元類幹掉,在類中把實例方法和類方法存在兩個不一樣的數組中?

答:行是確定可行的,可是在lookUpImpOrForward執行的時候就得標註上傳入的cls究竟是實例對象仍是類對象,這也就意味着在查找方法的緩存時一樣也須要判斷cls究竟是個啥。

假若該類存在同名的類方法和實例方法是該調用哪一個方法呢?這也就意味着還得給傳入的方法帶上是類方法仍是實例方法的標識,SEL並無帶上當前方法的類型(實例方法仍是類方法),參數又多加一個,而咱們如今的objc_msgSend()只接收了(id self, SEL _cmd, ...)這三種參數,第一個self就是消息的接收者,第二個就是方法,後續的...就是各式各樣的參數。

經過元類就能夠巧妙的解決上述的問題,讓各種各司其職,實例對象就幹存儲屬性值的事,類對象存儲實例方法列表,元類對象存儲類方法列表,完美的符合6大設計原則中的單一職責,並且忽略了對對象類型的判斷和方法類型的判斷能夠大大的提高消息發送的效率,而且在不一樣種類的方法走的都是同一套流程,在以後的維護上也大大節約了成本。

總結

本文從OC的消息機制分析了元類存在的意義,元類的存在巧妙的簡化了實例方法和類方法的調用流程,大大提高了消息發送的效率。

相關文章
相關標籤/搜索