Today we’re going to talk about eliminating sending messages and message forwarding in ObjC by looking at code and writing code. When we send a message to an object (instance object, class object), the object may not be able to handle it, and the result is a crash. Of course, crash can be prevented through message forwarding. Now we are left with a few puzzles: what is the mechanism for sending and processing messages? What are the timing and steps involved in message forwarding? (Why are there 2 steps instead of 3 as many people think?) What are some of the details of message forwarding? Below is my analysis of some open source code and through their own code practice, come to their own understanding and experience.

 id null = [NSNull null];
 [null setObject:@2 forKey:@"2"]; The 2017-12-08 10:40:34. 678705 + 0800test[8809:225907] -[NSNull setObject:forKey:]: 
unrecognized selector sent to instance 0x10bc2def0
Copy the code

Try to understand open source code

Send a message
void/id objc_msgSend(void /* id self, SEL op, ... _stret / _fpret /* * Sends a message with a simplereturn 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.
Copy the code

Objc_msgSend is implemented in objc-msG-x86.64. s with the following assembly code:

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 stillin 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
Copy the code

Some of the macros above are as follows:

GetIsaFast
.macro GetIsaFast
.if $0! = STRET testb $The $1, %a1b
	PN
	jnz	LGetIsaSlow_f
	movq	$$0x00007ffffffffff8, %r11
	andq	(%a1), %r11
.else
	testb	$The $1, %a2b
	PN
	jnz	LGetIsaSlow_f
	movq	$$0x00007ffffffffff8, %r11
	andq	(%a2), %r11
.endif
LGetIsaDone:	
.endmacro
Copy the code
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 .endmacroCopy the code
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	$The $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	$The $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
Copy the code
MethodTableLookup
.macro MethodTableLookup

	MESSENGER_END_SLOW
	
	SaveRegisters

	// _class_lookupMethodAndLoadCache3(receiver, selector, class)

	movq	$0, %a1
	movq	The $1, %a2
	movq	%r11, %a3
	call	__class_lookupMethodAndLoadCache3

	// IMP is now in %rax
	movq	%rax, %r11

	RestoreRegisters

.endmacro
Copy the code

Using the runtime API, the lowest level in the open source code, you can write the following pseudo-code line by line, as follows

id objc_msgSend(id self, SEL _cmd,…)
id objc_msgSend(id self, SEL _cmd,...) {1.if(! self)return nil; 
  ② Class cls = self->getIsa();
     IMP imp = nil;
  ③ imp = cache_getImp(cls, sel);   
     if (imp) returnimp; (4) imp = _class_lookupMethodAndLoadCache3 (self, _cmd, CLS);return imp;
}

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    returnlookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } // Skip the "optimistic" lockless cache lookup processCopy the code

If the object is nil, return nil. The cache cache_getimp (Class CLS, SEL SEL) macro contains and calls this code. Try to find sel IMP, it is possible to return _objc_msgForward_impcache(? More on that later. MethodTableLookup finally calls lookUpImpOrForward, which attempts to find sel imPs in all method_list_t in method_array_t. May return _objc_msgForward_impcache(? More on that later). In addition, we can guess that the definition of IMP in ObjC is typedef id (*IMP)(…). Or id (*IMP)(id object, SEL SEL…) (The return value may also be a structure or floating-point number).

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; }Copy the code

Briefly describe the process of finding this function through key points. Perform starting POINT A * starting point A method list locking (query read and dynamic add modification methods are mutually exclusive) and try to ignore GC SEL

  1. fromcache_tFind sel corresponding IMP, if found, directly return, may directly return_objc_msgForward_impcache;
  2. In the list of all methods (self, categorys), one by one is found using dichotomy or traversalnameThe attribute value is selmethod_t(Method), if found, use SEL as the key to save Methodcache_tExecute IMP in Mehtod directly.
static method_t *search_method_list(const method_list_t *mlist, {int isFixedUp = 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;
}
Copy the code
  1. Loop through the parent class until NSObject(parent class nil) passes_cache_getMethodMethod (return 1, IMP or nil) in the parent classcache_tLook for sel keysmethod_t, if at this timemethod_tNot 1(imp property isWhen _objc_msgForward_impcache methodIs 1), prove that the parent class has the record of executing this method, add its own cache, call directly, if is 1, stop looking for. Then continue to look in the list of all the methods in the parent class. If you find IMP, add your own cache and execute it.
  2. If not found, try calling its own_class_resolveMethodDynamically add method implementations to class or metaclass objects. If method is successfully added, the record has already been added, and the execution starts from the starting point A again.
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); }}}Copy the code

** If sel is not found at this time, add _objc_msgForward_impcache to the cache as sel implementation, and return _objc_msgForward_impcache. ** This also means that the next time the SEL message is received, _objc_msgForward_impcache will be returned directly from the cache.

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); }}Copy the code

_objc_msgForward_impcacheWhat is?

As can be seen from the above, when an object of a certain type cannot find the corresponding IMP IMP in the process of handling SEL messages for the first time, _objc_msgForward_impcache is included in the cache as the IMP corresponding to SEL (directly returned from the cache next time) and returned. Yes, it is the function pointer of message forwarding, that is, if the corresponding implementation IMP of sel of this class cannot be successfully found, the corresponding IMP of message forwarding will be executed. As can be seen from the above, _class_resolveMethod does not technically count as the next step in message forwarding because it is not triggered by _objc_msgForward_impcache. After the message is forwarded, the same object/class object is processed again to the message of the same name, which is forwarded directly (get sel imp from cache_t, that is, _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 NEfor 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
Copy the code

_objc_msgForward_impcache is an internal function pointer, _objc_msgForward or _objc_msgForward_stret are the actual message forwarding functions called, depending on the contents of the CPU’s status register. Also, the process is in _forward_handler or _forward_stret_handler. In the open source code, we found a default handler implementation. It looks like the familiar unrecognized selector sent to instance *, but is it really going to execute something so weak?

__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)) ? '+' : The '-', 
                object_getClassName(self), sel_getName(sel), self);
}
Copy the code

Code practice

Here is a piece of code that will crash;

- (void)viewDidLoad {
  [super viewDidLoad];
  id obj = [ASClassB new];
  
  [obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
  //[obj performSelector:@selector(exampleInvoke:) withObject:@"1"];
}
Copy the code

We put a break point there;

call (void)instrumentObjcMessageSends(YES)

performSelector:
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);
}
Copy the code

Through the source code, found that is doesNotRecognizeSelector: throw an exception to terminate the program and presents a hint! You can guess that the handler named Default did not actually execute. So how to verify the above message forwarding process is very simple, we can write a layer of simple message forwarding to prevent 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
Copy the code

We rewrite the ASClassB forwardingTargetForSelector: methods, try to forward the message to actually achieved exampleInvoke: ASClass an object of a class. As with the debugging steps above, we perform the methods on objA twice.

For the first time:

- 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:
Copy the code

The second:

- ASClassB NSObject performSelector:withObject:
- ASClassB ASClassB forwardingTargetForSelector:
- ASClassB ASClassB forwardingTargetForSelector:
+ ASClassA NSObject new
- ASClassA NSObject init
- ASClassA ASClassA exampleInvoke:
Copy the code

Can be found that the first point, did not perform methodSignatureForSelector: method, because forwardingTargetForSelector: method has returned to correctly deal with the object of the message; Second, when OBJ received the exampleInvoke: message the second time, it directly forwarded the message. The reason for this is that when sel imPs are not found for the first time, the message-forwarding IMP and SEL are placed in the cache_t of the class object/meta-object.

methodSignatureForSelector: & forwardInvocation:

Measured in not rewrite forwardingTargetForSelector: or the method provide object does not handle the message invalid (returns nil), will be performed in succession methodSignatureForSelector: and forwardInvocation: method.

#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
Copy the code

This simple demo implements correct message forwarding. Through rewriting methodSignatureForSelector: method returns a available method signature, through forwardInvocation: will incovation (described later) to complete a full process of sending a message. We can even rewrite these two methods to complete message forwarding of all unknown messages without crashing.

- (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
}
Copy the code

Later we also saw the forwardInvocation: invocation

- 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

Copy the code

Just to mention a few points, invokeWithTarget: In this case, it’s possible to forward to nil, because after all, nil will return nil if it receives any message. Then note that in the Invocation, the types for methodSignature here need only be set to “V@ :” or “v@”(without SEL), which is equivalent to ‘- (id)m; As long as the method parameters are not picked and set in the anInvocation, there is no array out of bounds and no variable transfer to the target. The system should have the parameters in a more efficient location and the Incocation fetch is just a lazy getter. In addition, the NSNull+NullSafe extension iterates through all classes to find a class object that can respond to an unknown message to forward the message, as well as cache optimization.

NSMethodSignature & NSInvocation

NSMethodSignature

A record of the type information for the return value and parameters of a method. Official documentation definition: A record of the values and parameters returned by a method.

Method m = class_getInstanceMethod(NSString.class, @selector(initWithFormat:));
const char *c = method_getTypeEncoding(m);
NSMethodSignature* sg = [[NSString new] methodSignatureForSelector:@selector(initWithFormat:)];
Copy the code

Output C and m, and get:

(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 (@) The '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) The '@'
        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 (@) The '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

Copy the code

C = “@24@0:8@16”, where the number represents the offset from the address. The second argument 0 is id self, argument 1 is SEL SEL, argument 2 is id arg. Objc_msgSend (id self, SEL op… objc_msgSend(id self, SEL op… */) arguments are in the same order… You can think of a method signature as a template record for a method. For Type Encoding, the following information is available:

#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'
Copy the code

Anyway, these different characters represent different types. For example, ‘:’ stands for SEL, proving that argument 1 really is SEL, @ stands for ‘id’, etc. For example – (BOOL) isKindOfClass: (Class) the CLS; Type encoding is “b@ :#”.

NSInvocation.

An Objective-C message rendered as an object. Messages rendered as objects can store all configurations of the message and be called directly to any object. Output the anInvocation from above:


//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 (@) The '@'
        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 (@) The '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
Copy the code

It can be seen that the meanings represented by the first few positions of the method signature described above are completely correct. It is also possible to build the invocation manually for dynamic execution of the multi-parameter method. Anyway, this class is pretty powerful, and we’ll talk about it in future articles.

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) 
Copy the code

As analyzed above, the index parameter of the method starts at 2.

Try to manually trigger message forwarding

As we already know, if method’s IMP is __objc_msgForward, message forwarding is triggered directly. The @selector(print) invocation of ASClassA will be replaced by __objc_msgForward and the @selector(forwardInvocation) imp will be replaced by our own invocation.

@implementation ASClassA
- (void)print {
  NSLog(@"ASClassA print");
}
Copy the code
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)];
}
Copy the code

The result is:

(LLDB) call (void) instrumentObjcMessageSends (YES) the 2017-12-10 23:20:47. 625463 + 0800test[12136:765892] hahhahahahhaha
(lldb) 
Copy the code

The execution process is:

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

Copy the code

The print method jumps directly to our custom function code implementation and the message is forwarded successfully. This is just a simple example of how powerful it would be if the custom invocation had a completely different invocation with the entire code based on the SEL name for each Invocation. In fact, some of the core parts of JSPatch use this approach to directly replace method implementations in certain classes.

Thanks for watching!! Please advise if you have any questions!!

reference

Github.com/RetVal/objc… Github.com/opensource-… Developer.apple.com/documentati… Decompile code for reference