今天我們主要以看代碼寫代碼的形式聊聊消ObjC中的發(fā)送消息和消息轉(zhuǎn)發(fā)。
當(dāng)我們向一個(gè)對(duì)象(實(shí)例對(duì)象、類對(duì)象)發(fā)送一條消息時(shí),對(duì)象可能是處理不了的,結(jié)果就是程序發(fā)生crash。當(dāng)然,通過(guò)消息轉(zhuǎn)發(fā)可以預(yù)防crash。現(xiàn)在我們就帶著幾個(gè)困惑:消息發(fā)送和處理的機(jī)制是什么樣的?消息轉(zhuǎn)發(fā)執(zhí)行的時(shí)機(jī)和包含的步驟是什么樣的?(為什么實(shí)際步驟是2步而不是很多人認(rèn)為的3步)?消息轉(zhuǎn)發(fā)的一些細(xì)節(jié)是什么樣的?下面是我分析一些開源代碼并通過(guò)自己的代碼實(shí)踐,得出的自己的一些理解和心得。
id null = [NSNull null];
[null setObject:@2 forKey:@"2"];
2017-12-08 10:40:34.678705+0800 test[8809:225907] -[NSNull setObject:forKey:]:
unrecognized selector sent to instance 0x10bc2def0
嘗試?yán)斫忾_源代碼
發(fā)送消息
void/id objc_msgSend(void /* id self, SEL op, ... */ ) //返回值為結(jié)構(gòu)體及浮點(diǎn)數(shù)時(shí)方法名有所不同_stret / _fpret
/*
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
objc_msgSend的實(shí)現(xiàn)在objc-msg-x86.64.s文件中的匯編代碼如下:
id objc_msgSend(id self, SEL _cmd,...)
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
*
********************************************************************/
ENTRY _objc_msgSend
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r11 = self->isa
CacheLookup NORMAL // calls IMP on success
NilTestSupport NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r11
MethodTableLookup %a1, %a2 // r11 = IMP
cmp %r11, %r11 // set eq (nonstret) for forwarding
jmp *%r11 // goto *imp
END_ENTRY _objc_msgSend
ENTRY _objc_msgSend_fixup
int3
END_ENTRY _objc_msgSend_fixup
上文中的一些宏如下:
GetIsaFast
.macro GetIsaFast
.if $0 != STRET
testb $$1, %a1b
PN
jnz LGetIsaSlow_f
movq $$0x00007ffffffffff8, %r11
andq (%a1), %r11
.else
testb $$1, %a2b
PN
jnz LGetIsaSlow_f
movq $$0x00007ffffffffff8, %r11
andq (%a2), %r11
.endif
LGetIsaDone:
.endmacro
NilTest
.macro NilTest //藏
.if $0 == SUPER || $0 == SUPER_STRET
error super dispatch does not test for nil
.endif
.if $0 != STRET
testq %a1, %a1
.else
testq %a2, %a2
.endif
PN
jz LNilTestSlow_f
.endmacro
CacheLookup
.macro CacheLookup
.if $0 != STRET && $0 != SUPER_STRET && $0 != SUPER2_STRET
movq %a2, %r10 // r10 = _cmd
.else
movq %a3, %r10 // r10 = _cmd
.endif
andl 24(%r11), %r10d // r10 = _cmd & class->cache.mask
shlq $$4, %r10 // r10 = offset = (_cmd & mask)<<4
addq 16(%r11), %r10 // r10 = class->cache.buckets + offset
.if $0 != STRET && $0 != SUPER_STRET && $0 != SUPER2_STRET
cmpq (%r10), %a2 // if (bucket->sel != _cmd)
.else
cmpq (%r10), %a3 // if (bucket->sel != _cmd)
.endif
jne 1f // scan more
// CacheHit must always be preceded by a not-taken `jne` instruction
CacheHit $0 // call or return imp
1:
// loop
cmpq $$1, (%r10)
jbe 3f // if (bucket->sel <= 1) wrap or miss
addq $$16, %r10 // bucket++
2:
.if $0 != STRET && $0 != SUPER_STRET && $0 != SUPER2_STRET
cmpq (%r10), %a2 // if (bucket->sel != _cmd)
.else
cmpq (%r10), %a3 // if (bucket->sel != _cmd)
.endif
jne 1b // scan more
// CacheHit must always be preceded by a not-taken `jne` instruction
CacheHit $0 // call or return imp
3:
// wrap or miss
jb LCacheMiss_f // if (bucket->sel < 1) cache miss
// wrap
movq 8(%r10), %r10 // bucket->imp is really first bucket
jmp 2f
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
1:
// loop
cmpq $$1, (%r10)
jbe 3f // if (bucket->sel <= 1) wrap or miss
addq $$16, %r10 // bucket++
2:
.if $0 != STRET && $0 != SUPER_STRET && $0 != SUPER2_STRET
cmpq (%r10), %a2 // if (bucket->sel != _cmd)
.else
cmpq (%r10), %a3 // if (bucket->sel != _cmd)
.endif
jne 1b // scan more
// CacheHit must always be preceded by a not-taken `jne` instruction
CacheHit $0 // call or return imp
3:
// double wrap or miss
jmp LCacheMiss_f
.endmacro
MethodTableLookup
.macro MethodTableLookup
MESSENGER_END_SLOW
SaveRegisters
// _class_lookupMethodAndLoadCache3(receiver, selector, class)
movq $0, %a1
movq $1, %a2
movq %r11, %a3
call __class_lookupMethodAndLoadCache3
// IMP is now in %rax
movq %rax, %r11
RestoreRegisters
.endmacro
使用開源代碼里最底層的runtime api,可以把上述過(guò)程下盡可能的逐行寫成如下偽代碼,如下
id objc_msgSend(id self, SEL _cmd,...)
id objc_msgSend(id self, SEL _cmd,...) {
① if (!self) return nil;
② Class cls = self->getIsa();
IMP imp = nil;
③ imp = cache_getImp(cls, sel);
if (imp) return imp;
④ imp = _class_lookupMethodAndLoadCache3(self, _cmd, cls);
return imp;
}
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
} // 跳過(guò)了“樂(lè)觀的”無(wú)鎖的查找cache過(guò)程
① NilTest宏,判斷對(duì)象是否為nil,若為 nil,直接返回 nil。
② GetIsaFast宏快速獲取到對(duì)象的 isa 指針地址(不同處理器架構(gòu)存放的位置不同)
③ CacheLookup宏_cache_getImp(Class cls, SEL sel)包含并調(diào)用了這塊代碼。嘗試尋找sel對(duì)應(yīng)的IMP,有可能返回_objc_msgForward_impcache(?下文會(huì)講到。
④ MethodTableLookup最終調(diào)用了lookUpImpOrForward方法,嘗試找method_array_t里所有method_list_t中的包含sel的method_t的IMP。有可能返回_objc_msgForward_impcache(?下文會(huì)講到)。
此外,我們可以猜測(cè)ObjC中IMP的定義為
typedef id (*IMP)(...)或者id (*IMP)(id object, SEL sel,...) (返回值也可能為結(jié)構(gòu)體或浮點(diǎn)數(shù))。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,vbool initialize, bool cache, bool resolver) 實(shí)現(xiàn)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP methodPC = nil;
Method meth;
bool triedResolver = NO;
methodListLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
methodPC = _cache_getImp(cls, sel);
if (methodPC) return methodPC;
}
// Check for freed class
if (cls == _class_getFreedObjectClass())
return (IMP) _freedHandler;
// Check for +initialize
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
// The lock is held to make method-lookup + cache-fill atomic
// with respect to method addition. Otherwise, a category could
// be added but ignored indefinitely because the cache was re-filled
// with the old value after the cache flush on behalf of the category.
retry:
methodListLock.lock();
// Ignore GC selectors
if (ignoreSelector(sel)) {
methodPC = _cache_addIgnoredEntry(cls, sel);
goto done;
}
// Try this class's cache.
methodPC = _cache_getImp(cls, sel);
if (methodPC) goto done;
// Try this class's method lists.
meth = _class_getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, cls, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
}
// Try superclass caches and method lists.
curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
if (meth) {
if (meth != (Method)1) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
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.
meth = _class_getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
methodListLock.unlock();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
_cache_addForwardEntry(cls, sel);
methodPC = _objc_msgForward_impcache;
done:
methodListLock.unlock();
// paranoia: look for ignored selectors with non-ignored implementations
assert(!(ignoreSelector(sel) && methodPC != (IMP)&_objc_ignored_method));
return methodPC;
}
通過(guò)關(guān)鍵點(diǎn)簡(jiǎn)述這個(gè)函數(shù)的查找過(guò)程.
執(zhí)行起點(diǎn)a
*起點(diǎn)a 方法列表加鎖(查詢讀取和動(dòng)態(tài)添加修改方法實(shí)現(xiàn)互斥),嘗試忽略GC sel
- 從
cache_t中尋找sel對(duì)應(yīng)的IMP,如果找到,直接返回, 可能直接返回_objc_msgForward_impcache; - 在所有方法列表中(自身,categorys)使用二分法或遍歷逐一尋找以
name屬性值為sel的method_t(Method),如果找到,以sel為鍵把method存入cache_t, 直接執(zhí)行mehtod里的IMP;
static method_t *search_method_list(const method_list_t *mlist, SEL sel) //藏
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
- 循環(huán)父類直到NSObject(父類為nil),通過(guò)
_cache_getMethod方法(返回1,IMP或nil)在父類的cache_t尋找以sel為鍵的method_t, 如果此時(shí)method_t不為1(imp屬性為_objc_msgForward_impcache時(shí)method為1),證明父類有執(zhí)行該方法的記錄,加入自己的緩存,直接調(diào)用,若為1,停止尋找。然后在父類的所有方法列表里繼續(xù)尋找,如果找到IMP,加入自己的緩存并執(zhí)行。 - 如果沒(méi)有找到,嘗試調(diào)用自身的
_class_resolveMethod動(dòng)態(tài)為類對(duì)象或元類對(duì)象里添加方法實(shí)現(xiàn)。如果成功添加了method,記錄已經(jīng)添加過(guò),重新從起點(diǎn)a出發(fā)執(zhí)行;
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);
}
}
}
如果這時(shí)候還沒(méi)找到sel對(duì)應(yīng)的IMP imp, 把_objc_msgForward_impcache當(dāng)做sel的實(shí)現(xiàn)一塊加入到緩存中,并返回_objc_msgForward_impcache。這也意味著,如果下次再收到該sel消息,將從緩存中直接返回_objc_msgForward_impcache。
void _cache_addForwardEntry(Class cls, SEL sel) //藏
{
cache_entry *smt;
smt = (cache_entry *)malloc(sizeof(cache_entry));
smt->name = sel;
smt->imp = _objc_msgForward_impcache;
if (! _cache_fill(cls, (Method)smt, sel)) { // fixme hack
// Entry not added to cache. Don't leak the method struct.
free(smt);
}
}
_objc_msgForward_impcache是什么?
上文中已經(jīng)可以看出,當(dāng)某種類型的對(duì)象第一次處理SEL sel消息過(guò)程中,無(wú)論如何也找不到對(duì)應(yīng)的IMP imp時(shí),便使得_objc_msgForward_impcache作為sel對(duì)應(yīng)的imp計(jì)入緩存(下一次直接從緩存中返回)并返回。沒(méi)錯(cuò),它就是消息轉(zhuǎn)發(fā)的函數(shù)指針,也就是說(shuō),無(wú)法順利找到該類sel對(duì)應(yīng)的實(shí)現(xiàn)imp時(shí),將執(zhí)行消息轉(zhuǎn)發(fā)對(duì)應(yīng)的imp。從上面也可以看出,嚴(yán)格意義上來(lái)講,_class_resolveMethod 因?yàn)椴⒉皇?code>_objc_msgForward_impcache觸發(fā)的,并不能算作消息轉(zhuǎn)發(fā)的后續(xù)步驟; 消息轉(zhuǎn)發(fā)后,該種對(duì)象/類對(duì)象再次處理到同名消息,將直接進(jìn)行消息轉(zhuǎn)發(fā)(從cache_t中拿到sel對(duì)應(yīng)的imp, 即_objc_msgForward_impcache)。
/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward and _objc_msgForward_stret are the externally-callable
* functions returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
* method caches.
*
********************************************************************/
.non_lazy_symbol_pointer
L_forward_handler:
.indirect_symbol __objc_forward_handler
.long 0
L_forward_stret_handler:
.indirect_symbol __objc_forward_stret_handler
.long 0
STATIC_ENTRY __objc_msgForward_impcache
// Method cache version
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band condition register is NE for stret, EQ otherwise.
MESSENGER_START
nop
MESSENGER_END_SLOW
jne __objc_msgForward_stret
jmp __objc_msgForward
END_ENTRY _objc_msgForward_impcache
ENTRY __objc_msgForward
// Non-struct return version
call 1f
1: popl %edx
movl L_forward_handler-1b(%edx), %edx
jmp *(%edx)
END_ENTRY __objc_msgForward
ENTRY __objc_msgForward_stret
// Struct return version
call 1f
1: popl %edx
movl L_forward_stret_handler-1b(%edx), %edx
jmp *(%edx)
END_ENTRY __objc_msgForward_stret
從源碼中可以看出,_objc_msgForward_impcache 只是個(gè)內(nèi)部的函數(shù)指針,會(huì)根據(jù)根據(jù)此時(shí) CPU 的狀態(tài)寄存器的內(nèi)容來(lái)繼續(xù)執(zhí)行 _objc_msgForward或者_objc_msgForward_stret, 這兩個(gè)才是真正的調(diào)用的消息轉(zhuǎn)發(fā)的函數(shù);且,對(duì)應(yīng)的處理過(guò)程在_forward_handler或_forward_stret_handler里。在開源代碼里,我們找到了一個(gè)默認(rèn)的handler實(shí)現(xiàn)。貌似輸出了我們熟悉的 unrecognized selector sent to instance *,但真的會(huì)執(zhí)行這樣薄弱的東西嗎?
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
代碼實(shí)踐
以下是一段會(huì)發(fā)生crash的代碼;
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [ASClassB new];
[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
//[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
}
我們?cè)谠撐恢么蛏蠑帱c(diǎn);

調(diào)試欄執(zhí)行
call (void)instrumentObjcMessageSends(YES), 繼續(xù)

在private/tmp文件夾中找到msgSends開頭的文件,便知道所有的發(fā)送的消息和對(duì)象的日志。(下圖截取了一部分)

可以看到,通過(guò)
performSelector:向ASClassA發(fā)送exampleInvoke:消息后,陸續(xù)調(diào)用了resolveInstanceMethod:``forwardingTargetForSelector:``methodSignatureForSelector:``class``doesNotRecognizeSelector:方法。
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
通過(guò)源碼發(fā)現(xiàn),是doesNotRecognizeSelector:拋出異常終止了程序并給出了提示!可以猜測(cè),實(shí)際上那個(gè)名字為default的handler并沒(méi)有執(zhí)行。那么如何驗(yàn)證上述消息轉(zhuǎn)發(fā)過(guò)程呢,很簡(jiǎn)單,我們可以寫一層層的簡(jiǎn)單的消息轉(zhuǎn)發(fā)來(lái)防止crash。
forwardingTargetForSelector:
#import "ASClassB.h"
#import "ASClassA.h"
#import <objc/runtime.h>
@implementation ASClassB
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(exampleInvoke:)) {
return [ASClassA new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
@implementation ASClassA
- (void)exampleInvoke:(NSString *)text {
NSLog(@"ASClassA receive exampleIncoke:");
}
@end
我們重寫了ASClassB的forwardingTargetForSelector:方法,嘗試把消息轉(zhuǎn)發(fā)給實(shí)際上已經(jīng)實(shí)現(xiàn)了exampleInvoke:的ASClass類的一個(gè)對(duì)象。和上文調(diào)試步驟一樣,我們對(duì)objA執(zhí)行兩次方法。

執(zhí)行結(jié)果:

第一次:
- ASClassB NSObject performSelector:withObject:
+ ASClassB NSObject resolveInstanceMethod:
+ ASClassB NSObject resolveInstanceMethod:
- ASClassB ASClassB forwardingTargetForSelector:
- ASClassB ASClassB forwardingTargetForSelector:
+ ASClassA NSObject initialize
+ ASClassA NSObject new
- ASClassA NSObject init
- ASClassA ASClassA exampleInvoke:
第二次:
- ASClassB NSObject performSelector:withObject:
- ASClassB ASClassB forwardingTargetForSelector:
- ASClassB ASClassB forwardingTargetForSelector:
+ ASClassA NSObject new
- ASClassA NSObject init
- ASClassA ASClassA exampleInvoke:
可以發(fā)現(xiàn),第一點(diǎn),沒(méi)有執(zhí)行methodSignatureForSelector:方法,因?yàn)?code>forwardingTargetForSelector:方法已經(jīng)返回了能正確處理消息的對(duì)象;第二點(diǎn),obj第二次收到exampleInvoke:消息時(shí),直接進(jìn)行進(jìn)行了消息轉(zhuǎn)發(fā)。原因正是上文中提到的首次未找到sel對(duì)應(yīng)的imp時(shí),直接把消息轉(zhuǎn)發(fā)的imp和sel一塊放在了類對(duì)象/元對(duì)象的cache_t中。
methodSignatureForSelector: & forwardInvocation:
實(shí)測(cè),在未重寫forwardingTargetForSelector:或該方法提供對(duì)象不能處理該消息時(shí)(返回nil無(wú)效),便會(huì)陸續(xù)執(zhí)行methodSignatureForSelector:和 forwardInvocation:方法。
#import "ASClassB.h"
#import "ASClassA.h"
#import <objc/runtime.h>
@implementation ASClassB
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(exampleInvoke:)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(exampleInvoke:)) {
[anInvocation invokeWithTarget:[ASClassA new]];
} else {
[super forwardInvocation:anInvocation];
}
}anInvocation invokeWithTarget:[ASClassA new]];
}
@end
這個(gè)簡(jiǎn)單的demo可以實(shí)現(xiàn)正確的消息轉(zhuǎn)發(fā)。通過(guò)重寫methodSignatureForSelector:方法返回一個(gè)可用的方法簽名,通過(guò)forwardInvocation:將incovation(后面介紹)完成一個(gè)完整的發(fā)送消息過(guò)程。我們甚至可以重寫這兩個(gè)方法完成所有未知消息的消息轉(zhuǎn)發(fā), 不再crash。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:nil];
#if DEBUG
NSLog(@"[%@ %@] unrecognized selector sent to instance %@", self.class, NSStringFromSelector(anInvocation.selector), self);
[NSException raise:@"UnrecognizedSelector" format:@"[%@ %@] unrecognized selector sent to instance %@", self.class, NSStringFromSelector(anInvocation.selector), self];
#endif
}
后來(lái)我們也看到了forwardInvocation:的調(diào)用過(guò)程
- ASClassB ASClassB forwardInvocation:
+ NSInvocation NSInvocation _invocationWithMethodSignature:frame:
+ NSInvocation NSObject alloc
- NSMethodSignature NSObject retain
- NSMethodSignature NSMethodSignature frameLength
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSMethodSignature NSMethodSignature frameLength
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSInvocation NSObject autorelease
- ASClassB ASClassB forwardInvocation:
- NSInvocation NSInvocation invokeWithTarget:
- NSInvocation NSInvocation setArgument:atIndex:
- NSMethodSignature NSMethodSignature numberOfArguments
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSMethodSignature NSMethodSignature _argInfo:
- NSMethodSignature NSMethodSignature _frameDescriptor
- NSInvocation NSInvocation invoke
提到幾個(gè)點(diǎn),invokeWithTarget:在這里,是可以轉(zhuǎn)發(fā)給nil的,畢竟nil收到任何消息后會(huì)直接返回nil。然后注意到,在這里的invocation調(diào)用過(guò)程,此處的methodSignature的types只需設(shè)成"v@:"或"v@"(如果不取SEL),相當(dāng)于`- (id)m;只要不在anInvocation里取和設(shè)方法參數(shù),并不會(huì)發(fā)生數(shù)組越界,也不會(huì)影響多個(gè)變量傳遞給新的target,系統(tǒng)執(zhí)行時(shí)應(yīng)該把參數(shù)放置在了一個(gè)更高效的位置,incocation取時(shí)也只相當(dāng)于一個(gè)懶加載的getter; 另外,NSNull+NullSafe擴(kuò)展采用了遍歷所有類來(lái)尋找能響應(yīng)未知消息的類對(duì)象來(lái)轉(zhuǎn)發(fā)消息,并做了緩存優(yōu)化。
簡(jiǎn)單講下 NSMethodSignature & NSInvocation
NSMethodSignature
A record of the type information for the return value and parameters of a method. 官方文檔定義:一個(gè)對(duì)于方法返回值和參數(shù)的記錄。
Method m = class_getInstanceMethod(NSString.class, @selector(initWithFormat:));
const char *c = method_getTypeEncoding(m);
NSMethodSignature* sg = [[NSString new] methodSignatureForSelector:@selector(initWithFormat:)];
輸出c和m, 得到:
(lldb) po c
"@24@0:8@16"
(lldb) po sg
<NSMethodSignature: 0x600000273880>
number of arguments = 3
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 0: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 2: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
c = "@24@0:8@16", 數(shù)字代表著相對(duì)于地址的偏移量,由下邊的sg可以看出,第一位@代表返回值(實(shí)際是argument -1),第二位 argument 0是id self, argument 1是SEL sel, argument 2是id arg。為什么會(huì)這樣,我們接下來(lái)會(huì)驗(yàn)證,這仿佛又與id objc_msgSend(id self, SEL op, ... */ )的參數(shù)順序是一致的...可以認(rèn)為方法簽名就是個(gè)方法的模板記錄。關(guān)于type encoding,有以下資料:
#define _C_ID '@'
#define _C_CLASS '#'
#define _C_SEL ':'
#define _C_CHR 'c'
#define _C_UCHR 'C'
#define _C_SHT 's'
#define _C_USHT 'S'
#define _C_INT 'i'
#define _C_UINT 'I'
#define _C_LNG 'l'
#define _C_ULNG 'L'
#define _C_LNG_LNG 'q'
#define _C_ULNG_LNG 'Q'
#define _C_FLT 'f'
#define _C_DBL 'd'
#define _C_BFLD 'b'
#define _C_BOOL 'B'
#define _C_VOID 'v'
#define _C_UNDEF '?'
#define _C_PTR '^'
#define _C_CHARPTR '*'
#define _C_ATOM '%'
#define _C_ARY_B '['
#define _C_ARY_E ']'
#define _C_UNION_B '('
#define _C_UNION_E ')'
#define _C_STRUCT_B '{'
#define _C_STRUCT_E '}'
#define _C_VECTOR '!'
#define _C_CONST 'r'
總之這些不同字符代表不同類型啦。例如':'代表SEL,證明了argument 1確實(shí)是sel,@代表'id'等。例如-(BOOL)isKindOfClass:(Class)cls;的type encoding為"B@:#"。
NSInvocation。
An Objective-C message rendered as an object.
呈現(xiàn)為對(duì)象的消息,可以存儲(chǔ)消息的所有配置和直接調(diào)用給任意對(duì)象(真tm是萬(wàn)物皆對(duì)象啊)。
輸出上文中得到的anInvocation:
//type: @v:@
id obj = [ASClassB new];
[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
----------------------------------------
id x;
id y;
id z;
[anInvocation getArgument:&x atIndex:0];
[anInvocation getArgument:&y atIndex:1];
[anInvocation getArgument:&z atIndex:2];
----------------------------------------
(lldb) po anInvocation
<NSInvocation: 0x604000460780>
return value: {v} void
target: {@} 0x6040000036e0
selector: {:} exampleInvoke:
argument 2: {@} 0x10e8ec340
(lldb) po x
<ASClassB: 0x60400000eb10>
(lldb) po anInvocation.selector
"exampleInvoke:"
(lldb) po NSStringFromSelector(y)
exampleInvoke:
(lldb) po z
1
(lldb) po anInvocation.methodSignature
<NSMethodSignature: 0x604000464c40>
number of arguments = 3
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 1: -------- -------- -------- --------
type encoding (:) ':'
flags {}
modifiers {}
frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
argument 2: -------- -------- -------- --------
type encoding (@) '@'
flags {isObject}
modifiers {}
frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
由此可以看出上文描述方法簽名前幾位位置代表的意義是完全正確的。
此外我們也可以自己手動(dòng)構(gòu)建invocation,實(shí)現(xiàn)多參數(shù)方法的動(dòng)態(tài)執(zhí)行。總之這個(gè)類很強(qiáng)大,后續(xù)文章我們還會(huì)提到。
NSString *text = @"string";
SEL sel = @selector(stringByAppendingString:);
NSMethodSignature *sg = [text methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sg];
invocation.target = text;
invocation.selector = sel;
id p = @"SS";
[invocation setArgument:&p atIndex:2];
id r;
[invocation invoke];
if (sg.methodReturnLength) {
[invocation getReturnValue:&r];
}
-----------------------------------------------
(lldb) po r
stringSS
(lldb)
和上面分析的一樣,方法的參數(shù)index從2開始。
嘗試手動(dòng)觸發(fā)消息轉(zhuǎn)發(fā)
前面我們已經(jīng)知道,如果method的imp為__objc_msgForward, 將直接觸發(fā)消息轉(zhuǎn)發(fā)。
下面我們直接替換ASClassA的@selector(print)的實(shí)現(xiàn)為__objc_msgForward,然后替換該類@selector(forwardInvocation:)對(duì)應(yīng)的imp為我們自己實(shí)現(xiàn)的函數(shù)。
@implementation ASClassA
- (void)print {
NSLog(@"ASClassA print");
}
void forward(id obj, SEL sel, NSInvocation *invo) {
if (invo.selector == @selector(print)) {
NSLog(@"hahhahahahhaha");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
class_replaceMethod(ASClassA.class, @selector(print), _objc_msgForward, "v@:");
class_replaceMethod(ASClassA.class, @selector(forwardInvocation:), (IMP)forward,"v@:@");
ASClassA *obj = [ASClassA new];
[obj performSelector:@selector(print)];
}
結(jié)果為:
(lldb) call (void)instrumentObjcMessageSends(YES)
2017-12-10 23:20:47.625463+0800 test[12136:765892] hahhahahahhaha
(lldb)
執(zhí)行過(guò)程為:
ASClassA NSObject performSelector:
- ASClassA ASClassA print
- ASClassA NSObject forwardingTargetForSelector:
- ASClassA NSObject forwardingTargetForSelector:
- ASClassA NSObject methodSignatureForSelector:
- ASClassA NSObject methodSignatureForSelector:
...
- ASClassA ASClassA forwardInvocation:
print方法直接跳到了我們的自定義函數(shù)代碼實(shí)現(xiàn)上,消息轉(zhuǎn)發(fā)成功。上述只是一個(gè)簡(jiǎn)單的例子,如果自定義的函數(shù)里根據(jù)每個(gè)invocation的SEL名字動(dòng)態(tài)化新建一個(gè)包含完整代碼完全不同的invocation,功能將會(huì)異常強(qiáng)大。實(shí)際上JSPatch的某些核心部分也正是使用了這種方式直接替換掉某些類里的方法實(shí)現(xiàn)。
謝謝觀看!!如有問(wèn)題請(qǐng)多指教!!
參考文獻(xiàn)
https://github.com/RetVal/objc-runtime
https://github.com/opensource-apple/objc4
https://developer.apple.com/documentation
可以參考的反編譯代碼