用程式碼理解ObjC中的傳送訊息和訊息轉發

Assuner發表於2017-12-21

今天我們主要以看程式碼寫程式碼的形式聊聊消ObjC中的傳送訊息和訊息轉發。 當我們向一個物件(例項物件、類物件)傳送一條訊息時,物件可能是處理不了的,結果就是程式發生crash。當然,通過訊息轉發可以預防crash。現在我們就帶著幾個困惑:訊息傳送和處理的機制是什麼樣的?訊息轉發執行的時機和包含的步驟是什麼樣的?(為什麼實際步驟是2步而不是很多人認為的3步)?訊息轉發的一些細節是什麼樣的?下面是我分析一些開原始碼並通過自己的程式碼實踐,得出的自己的一些理解和心得。

 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檔案中的彙編程式碼如下:

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,可以把上述過程下儘可能的逐行寫成如下虛擬碼,如下

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 sel

  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)

/********************************************************************
*
* 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的程式碼;

- (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… 可以參考的反編譯程式碼

相關文章