面試遇到Runtime的第三天-消息轉發

若是閱讀過前面的兩篇文章,我相信你必定對runtime有了一些本身的理解。本文就要切入正題,所謂Objective-C是一個動態的語言,他的主要核心就是消息轉發和傳遞。因此咱們瞭解runtime,也必定要明白他的核心(消息轉發)的實現原理。面試

消息

對於消息這個概念呢,咱們能夠簡單理解爲:調用一個對象的方法,就是給這個對象發消息。
好比下面的兩行代碼是等價的,array調用insert方法,並不會當即執行insert,而是在運行時給array發送了一條insert消息,至於這個消息是否會由array執行,或者是否能執行,都要在運行的時候才能決定。緩存

[array insertObject:foo atIndex:5];

objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
複製代碼

因此,消息傳遞的關鍵就在於objc_msgSend這個方法的定義bash

消息傳遞

在message.h文件中找到objc_msgSend的定義,這是一個可變參數函數,可是有兩個默認的參數,這兩參數是在編譯期自動生成的。函數

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
複製代碼
  • self: 調用當前方法的對象
  • _cmd 被調用方法的SEL

他的第二個參數類型是SEL,SEL在OC中就是selector方法選擇器oop

typedef struct objc_selector *SEL;
複製代碼

全部消息傳遞中的消息都會被轉換成一個objc_selector做爲objc_msgSend的參數,因此,OC中方法調用的過程,就是利用objc_msgSend向對象發送消息,而後根據發送的SEL找到具體的函數實現IMP,最後調用。post

這裏有幾個知識點須要注意:ui

  1. 實例方法:objc_msgSend(對象,sel)
  1. 類方法:objc_msgSend(類,sel)
  1. 父類:則是使用objc_msgSendSuper ,super和self指向一個對象,可是self是類隱藏參數,而super只是預編譯指令符號,做用就是不通過本類的方法列表,直接經過superClass的方法列表去查找,而後利用自己(objc_super->receiver)去調用

關於objc_msgSendSuper的調用在runtime的第一篇文章中的實戰中就分析過,能夠再回頭重溫一下spa

IMP

IMP本質上就是一個函數指針,在IMP中有兩個默認的參數id和SEL,和objc_msgSend()函數的參數同樣。debug

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
複製代碼

runtime提供了不少對於IMP的操做API,好比咱們比較熟悉的method swizzling就是經過這些API實現的。指針

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
OBJC_EXPORT void
method_getArgumentType(Method _Nonnull m, unsigned int index, 
                       char * _Nullable dst, size_t dst_len) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
...
複製代碼

獲取IMP

經過定義在NSObject中的方法,能夠獲取IMP,不只實例對象能夠調用,類對象也一樣能夠調用。

- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;
複製代碼

SEL(objc_selector)

objc_selector是一個映射到方法的C字符串。須要注意的是@selector()選擇子只與函數名有關。即便方法名字相同而參數類型不一樣或是參數個數不一樣也會致使它們具備相同的方法選擇器。因爲這點特性,也致使了OC不支持函數重載
在runtime中維護了一個SEL的表,這個表存儲SEL不按照類來存儲,只要相同的SEL都會被看做是一個,因此,不一樣類中同名的方法也是同一個SEL

method_t

源碼中查看method的定義,一樣也是一個結構體,其中包含了IMP,能夠看到只記錄了方法名字並無方法參數,也證明了上面說的選擇子只與函數名有關

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
複製代碼

這裏還可能會被問到一個面試題:
問:申明在.h文件中,而在.m中沒有實現中的方法,會被編譯到method list中嘛?
答:不會,編譯時只會將 Xcode中Compile Sources中.m文件聲明的方法編譯到method list中。

objc_msgSend

objc_msgSend函數究竟會幹什麼事情呢?

總結一下objc_msgSend會作如下幾件事情:

  1. 檢測這個 selector是否是要忽略的。
  2. 檢查target是否是爲nil。

若是這裏有相應的nil的處理函數,就跳轉到相應的函數中。 若是沒有處理nil的函數,就自動清理現場並返回。這一點就是爲什麼在OC中給nil發送消息不會崩潰的緣由

  1. 肯定不是給nil發消息以後,在該class的緩存中查找方法對應的IMP實現。

若是找到,就跳轉進去執行。 若是沒有找到,就在方法分發表裏面繼續查找,一直找到NSObject爲止。

  1. 若是尚未找到,那就須要開始消息轉發階段了。至此,消息傳遞階段完成。這一階段主要完成的是經過SEL快速查找IMP的過程。

objc_msgSend函數源碼解析

咱們以objc-msg-arm64.s文件爲例,他的實現實際上是一段彙編代碼,一眼看過去懵懵的,這裏其實沒有必要徹底看懂(可能徹底看不懂的機率更大),因此咱們就看幾個關鍵詞理解它的大概流程就行了。
搜索一下msgSend這個關鍵詞,就能夠找到咱們想要的那部分代碼(這裏註釋寫的仍是比較好的)

/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17
 * x16 reserved for our use but not used
 *
 ********************************************************************/

#if SUPPORT_TAGGED_POINTERS
	.data
	.align 3
	.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
	.fill 16, 8, 0
	.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
	.fill 256, 8, 0
#endif

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

仍是主要看註釋吧。。。(手動尷尬)
能夠看到這裏作了咱們上面說的,檢查target是否是爲nil,肯定不是nil以後,在該class的緩存中查找方法對應的IMP實現,就是 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
 *
 ********************************************************************/
 .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
複製代碼

這裏就要用到面試遇到Runtime的次日-isa和meta-Class中說到的objc_class結構體中的cache部分,爲了提升方法查找的效率會先在緩存中查找,若是未命中就跳入CheckMiss(都是彙編代碼實現,就不展開說了,瞭解過程就行,根據前面傳入的參數NORMAL,知道下一步進入_objc_msgSend_uncached),若是命中就執行CacheHit,返回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
複製代碼

而_objc_msgSend_uncached中主要就是調用了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中的主要工做是去調用__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 涉及不少函數的調用,因此咱們就不一一貼代碼了,總結一下流程,撿重要的點說明一下具體調用:

  1. 無鎖的緩存查找(由於參數cache傳入NO,因此這裏是直接跳過的)
  2. 若是類沒有實現(isRealized)或者初始化(isInitialized),實現或者初始化類(貼一點兒精簡版的代碼)

在這個過程當中,會把編譯器存儲在bits(objc_class中定義的bits,還不熟悉的能夠看下上一篇文章)中的class_ro_t取出來,而後建立class_rw_t,並把ro賦值給rw,成爲rw的一個成員變量,最後把rw設置給bits,替代以前bits中的ro

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;

    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }

    isMeta = ro->flags & RO_META;

    rw->version = isMeta ? 7 : 0;  // old runtime went up to 6

    cls->chooseClassArrayIndex();

    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));

    cls->superclass = supercls;
    cls->initClassIsa(metacls);
    cls->setInstanceSize(ro->instanceSize);

    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    methodizeClass(cls);

    return cls;
}

複製代碼

在上面源碼中還有兩個函數addSubclass和addRootClass,這兩個函數是把當前類的子類串成一個列表,因此,咱們是能夠經過class_rw_t獲取到當前類的全部子類。

初始化rw以後,rw的method list ,protocol list,property list都是空的,須要調用methodizeClass函數進行賦值。函數中會把ro中的list取出賦值給rw,咱們在運行時動態修改的就是rw.因此ro中存儲的是編譯時決定的原數據,rw纔是運行時修改的數據。

static void methodizeClass(Class cls)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/); } 複製代碼
  1. 加鎖
  2. 緩存以及當前類中方法的查找
  3. 嘗試查找父類的緩存以及方法列表
  4. 沒有找到實現,嘗試方法解析器
  5. 進行消息轉發
  6. 解鎖、返回實現

動態方法解析

當一個方法沒有實現時,也就是在cache list和繼承關係的method list中,都沒有找到對應的方法,這時會進入到消息轉發階段。
在進入轉發階段前,會有一次動態添加方法實現的機會:經過重寫下面兩個方法,動態添加實例方法和類方法,這兩方法若是都返回NO,則進入消息轉發。

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼

在進行消息轉發前,還能夠經過forwardingTargetForSelector:方法,將消息轉發給其餘對象。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorName = NSStringFromSelector(aSelector); if ([selectorName isEqualToString:@"selector"]) {
        return object;
    }
    return [super forwardingTargetForSelector:aSelector]; 
     
 }
複製代碼

消息轉發

若是forwardingTargetForSelector:方法未實現,會進入消息轉發流程。

  1. 生成方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
複製代碼

2.經過forwardInvocation將消息轉發給另一個對象,該方法接收一個NSInvocation類型的參數, NSInvocation對象中包含原始消息及參數

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:someOtherObject]; 
        
    } else {
        [super forwardInvocation:anInvocation]; }

}
複製代碼

調用invokeWithTarget方法後,原方法的返回值將被返回給調用方。

模擬多繼承

能夠經過消息轉發機制來模擬多繼承,分屬不一樣繼承系統裏面的兩個類,經過消息轉發,將實現重定向到其餘類中,以實現多繼承。

相關文章
相關標籤/搜索