原文連結OC訊息機制和super關鍵字
訊息傳送
在Objective-C裡面呼叫一個方法[object method]
,執行時會將它翻譯成objc_msgSend(id self, SEL op, ...)
的形式。
objc_msgSend
objc_msgSend
的實現在objc-msg-arm.s
、objc-msg-arm64.s
等檔案中,是通過彙編實現的。這裡主要看在arm64
即objc-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
複製程式碼
上面的流程可能是這樣的:
從CacheLookup
的註釋有兩處:
calls imp or objc_msgSend_uncached
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
,它的函式實現比較長,但是註釋寫的非常清楚。它的實現主要由以下幾步(這裡直接從快取獲取開始):
- 通過
cache_getImp
從快取中獲取方法,有則返回,否則進入第2步; - 通過
getMethodNoSuper_nolock
從類的方法列表中獲取,有加入快取中並返回,否則進入第3步; - 通過父類的快取和父類的方法列表中尋找是否有對應的imp,此時會進入一個
for
迴圈,沿著類的父類一直往上找,直接找到NSObject為止。如果找到返回,否則進入第4步; - 進入方法決議(method resolve)的過程即呼叫
_class_resolveMethod
,如果失敗,進入第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_msgForward
在objc
中並沒有其相關實現,只能看到_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:
處打個斷點,檢視一下呼叫棧:
_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];
複製程式碼
程式碼執行結果和訊息重定向測試的執行結果一致。_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]
的時候,呼叫了那些方法
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
複製程式碼