用代碼理解ObjC中的發送消息和消息轉發

今天咱們主要以看代碼寫代碼的形式聊聊消ObjC中的發送消息和消息轉發。 當咱們向一個對象(實例對象、類對象)發送一條消息時,對象多是處理不了的,結果就是程序發生crash。固然,經過消息轉發能夠預防crash。如今咱們就帶着幾個困惑:消息發送和處理的機制是什麼樣的?消息轉發執行的時機和包含的步驟是什麼樣的?(爲何實際步驟是2步而不是不少人認爲的3步)?消息轉發的一些細節是什麼樣的?下面是我分析一些開源代碼並經過本身的代碼實踐,得出的本身的一些理解和心得。git

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

嘗試理解開源代碼

發送消息
void/id objc_msgSend(void /* id self, SEL op, ... */ )   //返回值爲結構體及浮點數時方法名有所不一樣_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的實如今objc-msg-x86.64.s文件中的彙編代碼以下:github

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

上文中的一些宏以下:api

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,能夠把上述過程下儘量的逐行寫成以下僞代碼,以下數組

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*/);
}  // 跳過了「樂觀的」無鎖的查找cache過程
複製代碼

NilTest宏,判斷對象是否爲nil,若爲 nil,直接返回 nil。 ② GetIsaFast宏快速獲取到對象的 isa 指針地址(不一樣處理器架構存放的位置不一樣) ③ CacheLookup_cache_getImp(Class cls, SEL sel)包含並調用了這塊代碼。嘗試尋找sel對應的IMP,有可能返回_objc_msgForward_impcache(?下文會講到。 ④ MethodTableLookup最終調用了lookUpImpOrForward方法,嘗試找method_array_t裏全部method_list_t中的包含sel的method_t的IMP。有可能返回_objc_msgForward_impcache(?下文會講到)。 此外,咱們能夠猜想ObjC中IMP的定義爲 typedef id (*IMP)(...)或者id (*IMP)(id object, SEL sel,...) (返回值也可能爲結構體或浮點數)。緩存

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,vbool initialize, bool cache, bool resolver) 實現
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; } 複製代碼

經過關鍵點簡述這個函數的查找過程. 執行起點a *起點a 方法列表加鎖(查詢讀取和動態添加修改方法實現互斥),嘗試忽略GC selbash

  1. cache_t中尋找sel對應的IMP,若是找到,直接返回, 可能直接返回_objc_msgForward_impcache
  2. 在全部方法列表中(自身,categorys)使用二分法或遍歷逐一尋找以name屬性值爲sel的method_t(Method),若是找到,以sel爲鍵把method存入cache_t, 直接執行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;
}
複製代碼
  1. 循環父類直到NSObject(父類爲nil),經過_cache_getMethod方法(返回1,IMP或nil)在父類的cache_t尋找以sel爲鍵的method_t, 若是此時method_t不爲1(imp屬性爲_objc_msgForward_impcache時method爲1),證實父類有執行該方法的記錄,加入本身的緩存,直接調用,若爲1,中止尋找。而後在父類的全部方法列表裏繼續尋找,若是找到IMP,加入本身的緩存並執行。
  2. 若是沒有找到,嘗試調用自身的_class_resolveMethod動態爲類對象或元類對象裏添加方法實現。若是成功添加了method,記錄已經添加過,從新從起點a出發執行;
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);
        }
    }
}
複製代碼

**若是這時候還沒找到sel對應的IMP imp, 把_objc_msgForward_impcache當作sel的實現一塊加入到緩存中,並返回_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是什麼?

上文中已經能夠看出,當某種類型的對象第一次處理SEL sel消息過程當中,不管如何也找不到對應的IMP imp時,便使得_objc_msgForward_impcache做爲sel對應的imp計入緩存(下一次直接從緩存中返回)並返回。沒錯,它就是消息轉發的函數指針,也就是說,沒法順利找到該類sel對應的實現imp時,將執行消息轉發對應的imp。從上面也能夠看出,嚴格意義上來說,_class_resolveMethod 由於並非_objc_msgForward_impcache觸發的,並不能算做消息轉發的後續步驟; 消息轉發後,該種對象/類對象再次處理到同名消息,將直接進行消息轉發(從cache_t中拿到sel對應的imp, 即_objc_msgForward_impcache)app

/********************************************************************
*
* 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 只是個內部的函數指針,會根據根據此時 CPU 的狀態寄存器的內容來繼續執行 _objc_msgForward或者_objc_msgForward_stret, 這兩個纔是真正的調用的消息轉發的函數;且,對應的處理過程在_forward_handler或_forward_stret_handler裏。在開源代碼裏,咱們找到了一個默認的handler實現。貌似輸出了咱們熟悉的 unrecognized selector sent to instance *,但真的會執行這樣薄弱的東西嗎?函數

__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);
}
複製代碼

代碼實踐

如下是一段會發生crash的代碼;oop

- (void)viewDidLoad {
  [super viewDidLoad];
  id obj = [ASClassB new];
  
  [obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
  //[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
}
複製代碼

咱們在該位置打上斷點;

屏幕快照 2017-12-10 下午6.33.19.png
調試欄執行 call (void)instrumentObjcMessageSends(YES), 繼續

屏幕快照 2017-12-10 下午6.34.01.png
在private/tmp文件夾中找到msgSends開頭的文件,便知道全部的發送的消息和對象的日誌。(下圖截取了一部分)
屏幕快照 2017-12-10 下午6.43.51.png
能夠看到,經過 performSelector:向ASClassA發送 exampleInvoke:消息後,陸續調用了 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);
}
複製代碼

經過源碼發現,是doesNotRecognizeSelector:拋出異常終止了程序並給出了提示!能夠猜想,實際上那個名字爲default的handler並無執行。那麼如何驗證上述消息轉發過程呢,很簡單,咱們能夠寫一層層的簡單的消息轉發來防止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:方法,嘗試把消息轉發給實際上已經實現了exampleInvoke:的ASClass類的一個對象。和上文調試步驟同樣,咱們對objA執行兩次方法。

屏幕快照 2017-12-10 下午7.12.32.png
執行結果:
屏幕快照 2017-12-10 下午7.18.19.png

第一次:

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

能夠發現,第一點,沒有執行methodSignatureForSelector:方法,由於forwardingTargetForSelector:方法已經返回了能正確處理消息的對象;第二點,obj第二次收到exampleInvoke:消息時,直接進行進行了消息轉發。緣由正是上文中提到的首次未找到sel對應的imp時,直接把消息轉發的imp和sel一塊放在了類對象/元對象的cache_t中。

methodSignatureForSelector: & forwardInvocation:

實測,在未重寫forwardingTargetForSelector:或該方法提供對象不能處理該消息時(返回nil無效),便會陸續執行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
複製代碼

這個簡單的demo能夠實現正確的消息轉發。經過重寫methodSignatureForSelector:方法返回一個可用的方法簽名,經過forwardInvocation:將incovation(後面介紹)完成一個完整的發送消息過程。咱們甚至能夠重寫這兩個方法完成全部未知消息的消息轉發, 再也不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
}
複製代碼

後來咱們也看到了forwardInvocation:的調用過程

- 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

複製代碼

提到幾個點,invokeWithTarget:在這裏,是能夠轉發給nil的,畢竟nil收到任何消息後會直接返回nil。而後注意到,在這裏的invocation調用過程,此處的methodSignaturetypes只需設成"v@:"或"v@"(若是不取SEL),至關於`- (id)m;只要不在anInvocation裏取和設方法參數,並不會發生數組越界,也不會影響多個變量傳遞給新的target,系統執行時應該把參數放置在了一個更高效的位置,incocation取時也只至關於一個懶加載的getter; 另外,NSNull+NullSafe擴展采用了遍歷全部類來尋找能響應未知消息的類對象來轉發消息,並作了緩存優化。

簡單講下 NSMethodSignature & NSInvocation

NSMethodSignature

A record of the type information for the return value and parameters of a method. 官方文檔定義:一個對於方法返回值和參數的記錄。

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", 數字表明着相對於地址的偏移量,由下邊的sg能夠看出,第一位@表明返回值(實際是argument -1),第二位 argument 0是id self, argument 1是SEL sel, argument 2是id arg。爲何會這樣,咱們接下來會驗證,這彷彿又與id objc_msgSend(id self, SEL op, ... */ )的參數順序是一致的...能夠認爲方法簽名就是個方法的模板記錄。關於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確實是sel,@表明'id'等。例如-(BOOL)isKindOfClass:(Class)cls;的type encoding爲"B@:#"。

NSInvocation。

An Objective-C message rendered as an object. 呈現爲對象的消息,能夠存儲消息的全部配置和直接調用給任意對象(真tm是萬物皆對象啊)。 輸出上文中獲得的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}
複製代碼

由此能夠看出上文描述方法簽名前幾位位置表明的意義是徹底正確的。 此外咱們也能夠本身手動構建invocation,實現多參數方法的動態執行。總之這個類很強大,後續文章咱們還會提到。

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

和上面分析的同樣,方法的參數index從2開始。

嘗試手動觸發消息轉發

前面咱們已經知道,若是method的imp爲__objc_msgForward, 將直接觸發消息轉發。 下面咱們直接替換ASClassA的@selector(print)的實現爲__objc_msgForward,而後替換該類@selector(forwardInvocation:)對應的imp爲咱們本身實現的函數。

@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)];
}
複製代碼

結果爲:

(lldb) call (void)instrumentObjcMessageSends(YES)
2017-12-10 23:20:47.625463+0800 test[12136:765892] hahhahahahhaha
(lldb) 
複製代碼

執行過程爲:

ASClassA NSObject performSelector:
- ASClassA ASClassA print
- ASClassA NSObject forwardingTargetForSelector:
- ASClassA NSObject forwardingTargetForSelector:
- ASClassA NSObject methodSignatureForSelector:
- ASClassA NSObject methodSignatureForSelector:
...
- ASClassA ASClassA forwardInvocation:

複製代碼

print方法直接跳到了咱們的自定義函數代碼實現上,消息轉發成功。上述只是一個簡單的例子,若是自定義的函數里根據每一個invocation的SEL名字動態化新建一個包含完整代碼徹底不一樣的invocation,功能將會異常強大。實際上JSPatch的某些核心部分也正是使用了這種方式直接替換掉某些類裏的方法實現。

謝謝觀看!!若有問題請多指教!!

參考文獻

github.com/RetVal/objc… github.com/opensource-… developer.apple.com/documentati… 能夠參考的反編譯代碼

相關文章
相關標籤/搜索