OC訊息機制和super關鍵字

NeroXie發表於2019-03-12

原文連結OC訊息機制和super關鍵字

訊息傳送

在Objective-C裡面呼叫一個方法[object method],執行時會將它翻譯成objc_msgSend(id self, SEL op, ...)的形式。

objc_msgSend

objc_msgSend的實現在objc-msg-arm.sobjc-msg-arm64.s等檔案中,是通過彙編實現的。這裡主要看在arm64objc-msg-arm64.s的實現。由於彙編不熟,裡面的實現只能連看帶猜。

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START

	cmp	x0, #0			// nil check and tagged pointer check
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

LNilOrTagged:
    /* nil check,如果為空就是呼叫LReturnZero,LReturnZero裡呼叫MESSENGER_END_NIL*/
	b.eq	LReturnZero		// nil check

	// tagged
	mov	x10, #0xf000000000000000
	cmp	x0, x10
	b.hs	LExtTag
	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]
	b	LGetIsaDone

LExtTag:
	// 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
	
LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	MESSENGER_END_NIL
	ret

	END_ENTRY _objc_msgSend
複製程式碼

上面的流程可能是這樣的:

objc_msgsend

CacheLookup的註釋有兩處:

  1. calls imp or objc_msgSend_uncached
  2. Locate the implementation for a selector in a class method cache.

即使看不懂彙編程式碼,但是從上面的註釋我們可以猜測,訊息機制會先從快取中去查詢。

__objc_msgSend_uncached

通過方法名我們可以知道,沒有快取的時候應該會執行__objc_msgSend_uncached

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band x16 is the class to search
	
	MethodTableLookup
	br	x17

	END_ENTRY __objc_msgSend_uncached
複製程式碼

這裡的MethodTableLookup裡涉及到objc-runtime-new.mm檔案中的_class_lookupMethodAndLoadCache3。該函式會呼叫lookUpImpOrForward函式。

lookUpImpOrForward

lookUpImpOrForward會返回一個imp,它的函式實現比較長,但是註釋寫的非常清楚。它的實現主要由以下幾步(這裡直接從快取獲取開始):

  1. 通過cache_getImp從快取中獲取方法,有則返回,否則進入第2步;
  2. 通過getMethodNoSuper_nolock從類的方法列表中獲取,有加入快取中並返回,否則進入第3步;
  3. 通過父類的快取和父類的方法列表中尋找是否有對應的imp,此時會進入一個for迴圈,沿著類的父類一直往上找,直接找到NSObject為止。如果找到返回,否則進入第4步;
  4. 進入方法決議(method resolve)的過程即呼叫_class_resolveMethod,如果失敗,進入第5步;
  5. 在快取、當前類、父類以及方法決議都沒有找到的情況下,Objective-C還為我們提供了最後一次翻身的機會,呼叫_objc_msgForward_impcache進行方法轉發,如果找到便加入快取;如果沒有就crash。

上述過程中有幾個比較重要的函式:

_class_resolveMethod

void _class_resolveMethod(Class cls, SEL sel, id inst) {
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製程式碼

上述函式會根據當前傳入的類的是不是一個元類,在_class_resolveInstanceMethod_class_resolveClassMethod中選擇一個進行呼叫。註釋也說明了這兩個方法的作用就是判斷當前類是否實現了 resolveInstanceMethod:或者resolveClassMethod:方法,然後用objc_msgSend執行上述方法。

_class_resolveClassMethod

_class_resolveClassMethod_class_resolveInstanceMethod實現類似,這裡就只看_class_resolveClassMethod的實現。

static void _class_resolveClassMethod(Class cls, SEL sel, id inst) {
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) {
         //沒有找到resolveClassMethod方法,直接返回。
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // 快取結果
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
    // 以下程式碼省略不影響閱讀                          
}
複製程式碼

_objc_msgForward_impcache

	STATIC_ENTRY __objc_msgForward_impcache

	MESSENGER_START
	nop
	MESSENGER_END_SLOW

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	x17, [x17, __objc_forward_handler@PAGEOFF]
	br	x17
	
	END_ENTRY __objc_msgForward
複製程式碼

_objc_msgForward_impcache用來進行訊息轉發,但是其真正的核心是呼叫_objc_msgForward

訊息轉發

關於_objc_msgForwardobjc中並沒有其相關實現,只能看到_objc_forward_handler。其實_objc_msgForward的實現是在CFRuntime.c中的,但是開源出來的CFRuntime.c並沒有相關實現,但是也不影響我們對真理的追求。

我們做幾個實驗來驗證訊息轉發。

訊息重定向測試

// .h檔案
@interface AObject : NSObject

- (void)sendMessage;

@end
// .m檔案
@implementation AObject

/** 驗證訊息重定向 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sendMessage)) {
         return [BObject new];
    }

    return [super forwardingTargetForSelector:aSelector];
}

@end

// .h檔案
@interface BObject : NSObject

- (void)sendMessage;

@end

// .m檔案
@implementation BObject

- (void)sendMessage {
    NSLog(@"%@ send message", self.class);
}

@end

// 呼叫
AObject *a = [AObject new];
[a sendMessage];
複製程式碼

執行結果:

2019-03-12 10:18:54.252949+0800 iOSCodeLearning[18165:5967575] BObject send message
複製程式碼

forwardingTargetForSelector:處打個斷點,檢視一下呼叫棧:

message_redirection

_CF_forwarding_prep_0___forwarding___這兩個方法會先被呼叫了,之後呼叫了forwardingTargetForSelector:

方法簽名測試

// .h檔案
@interface AObject : NSObject

- (void)sendMessage;

@end
// .m檔案
@implementation AObject

/** 訊息重定向 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
   return nil;
}

/** 方法簽名測試 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(sendMessage)) {
        return [BObject instanceMethodSignatureForSelector:@selector(sendMessage)];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    if (selector == @selector(sendMessage)) {
        [anInvocation invokeWithTarget:[BObject new]];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

@end

// .h檔案
@interface BObject : NSObject

- (void)sendMessage;

@end

// .m檔案
@implementation BObject

- (void)sendMessage {
    NSLog(@"%@ send message", self.class);
}

@end

// 呼叫
AObject *a = [AObject new];
[a sendMessage];
複製程式碼

method_signature

程式碼執行結果和訊息重定向測試的執行結果一致。_CF_forwarding_prep_0___forwarding___這兩個方法又再次被呼叫了,之後程式碼會先執行forwardingTargetForSelector:(訊息重定向),訊息重定向如果失敗後呼叫methodSignatureForSelector:forwardInvocation:方法簽名。所以說___forwarding___方法才是訊息轉發的真正實現。

crash測試

// .h檔案
@interface AObject : NSObject

- (void)sendMessage;

@end
// .m檔案
@implementation AObject

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
}

/** 驗證Crash */
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    if (aSelector == @selector(sendMessage)) {
        NSLog(@"%@ doesNotRecognizeSelector", self.class);
    }
}

@end

// .h檔案
@interface BObject : NSObject

- (void)sendMessage;

@end

// .m檔案
@implementation BObject

- (void)sendMessage {
    NSLog(@"%@ send message", self.class);
}

@end

// 呼叫
AObject *a = [AObject new];
[a sendMessage];
複製程式碼

程式碼執行結果肯定是crash,結合上面的程式碼我們知道訊息轉發會呼叫___forwarding___這個內部方法。___forwarding___方法呼叫順序是forwardingTargetForSelector:->methodSignatureForSelector:->doesNotRecognizeSelector:

我們用一張圖表示整個訊息傳送的過程:

訊息機制流程圖

super關鍵字

我們先檢視一下執行[super init]的時候,呼叫了那些方法

super_init

objc_msgSendSuper2的宣告在objc-abi.h

// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
複製程式碼

objc_super的定義如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
複製程式碼

從上面的定義我們可以知道receiver即訊息的實際接收者, super_class為指向當前類的父類。

所以該函式實際的操作是:從objc_super結構體指向的super_class開始查詢,直到會找到NSObject的方法為止。找到後以receiver去呼叫。當然整個查詢的過程還是和訊息傳送的流程一樣。

所以我們能理解為什麼下面這段程式碼執行的結果都是AObject了吧。雖然使用[super class],但是真正執行方法的物件還是AObject

// 程式碼
@implementation AObject

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@", [super class]);
        NSLog(@"%@", [self class]);
    }
    
    return self;
}

@end

// 執行結果
2019-03-12 19:44:46.003313+0800 iOSCodeLearning[34431:7234182] AObject
2019-03-12 19:44:46.003442+0800 iOSCodeLearning[34431:7234182] AObject
複製程式碼

相關文章