methods

In the Classes and Objects section, we learned that methods are stored in classes. So the question is, what does the method look like?

method_t

struct method_t {
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
}
Copy the code

Objc_class has a binary array member (methods) of type method_array_t. Unpacking the method_array_t type, we find the final method_t, which is what the method function itself looks like. So you can see that there are three properties in a method_t

  • Name: method name
  • Types: Encoding (contains arguments, return value types).TypeEncoding
  • Imp: method implementation

TypeEncoding

A set of method return value types, as well as encoding rules for parameter types, customized by Apple. The following figure

OC method call mechanism

objc_msgSend

In OC, the method call is eventually converted to an objc_msgSend call. This approach is called messaging, where messages are sent to method callers.

  • Message receiver: The object on which the method is invoked

  • Message name :@selector(xx)

    [a foo] // instance method objc_msgSend(a, @selector(foo)) [a foo] // class method objc_msgSend(objc_getClass(“A”),@selector(foo))

Objc_msgSend is divided into three main phases

  • Message is sent
  • Dynamic method parsing
  • forward

Message is sent

In Apple’s open source,objc_msgsend is implemented in assembly. assembly

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend
Copy the code
LNilOrTagged

If the message receiver is nil, return directly

CacheLookup

Make sure the message receiver is not empty and look in the cache. If the cache hits, call _objc_msgSend, if not. Call __objc_msgSend_uncached, MethodTableLookup, and _lookUpImpOrForward

lookUpImpOrForward
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { for (unsigned attempts = unreasonableClassCount();; ) { if (curClass->cache.isConstantOptimizedCache(/* strict */true)) { #if CONFIG_USE_PREOPT_CACHES imp = cache_getImp(curClass, sel); if (imp) goto done_unlock; curClass = curClass->cache.preoptFallbackClass(); #endif } else { // curClass method list. Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp(false); goto done; } if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; break; } } // Halt if there is a cycle in the superclass chain. if (slowpath(--attempts == 0)) { _objc_fatal("Memory corruption  in class list."); } // Superclass cache. imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } if (fastpath(imp)) { // Found the method in a superclass. Cache it in this class. goto done; } } // No implementation found. Try method resolver once. if (slowpath(behavior & LOOKUP_RESOLVER)) { behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); } done: if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { #if CONFIG_USE_PREOPT_CACHES while (cls->cache.isConstantOptimizedCache(/* strict */true)) { cls = cls->cache.preoptFallbackClass(); } #endif log_and_fill_cache(cls, imp, sel, inst, curClass); } done_unlock: runtimeLock.unlock(); if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { return nil; } return imp; }Copy the code

Here I’ve taken some of the code for lookUpImpOrForward. I can get that.

  • 1. Cache_getImp is retrieved from the class’s list of cache methods.
  • 2. No match, call getMethodNoSuper_nolock to try to find a method from the method list of the class.
    • 2.1 If a hit occurs, a call to cache_T :: INSERT caches the method into the message recipient cache list and returns IMP for the message recipient to invoke.
    • 2.2 If no match is found, perform Step 1 again.
    • 2.3 If no, go to 2.2
  • 2. Imp is not hit until the parent class is nil, then dynamic parsing resolveMethod_locked is entered
getMethodNoSuper_nolock
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }
}
Copy the code

You can see that in the search_method_list_inline method that searches the list of methods, a binary lookup is performed for the sorted list of methods. And unsorted, traversal search is used

Dynamic method parsing

resolveMethod_locked
static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertLocked(); ASSERT(cls->isRealized()); runtimeLock.unlock(); if (! cls->isMetaClass()) { // try [cls resolveInstanceMethod:sel] resolveInstanceMethod(inst, sel, cls); } else { // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] resolveClassMethod(inst, sel, cls); if (! lookUpImpOrNilTryCache(inst, sel, cls)) { resolveInstanceMethod(inst, sel, cls); } } // chances are that calling the resolver have populated the cache // so attempt using it return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); }Copy the code

Dynamic analysis, according to the class or metaclass attributes to call resolveInstanceMethod respectively, resolveClassMethod

Implement dynamic method resolution

For every NSObject class, there are the following methods that we can handle dynamic method resolution.

+ (BOOL)resolveClassMethod:(SEL) SEL OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); + (BOOL)resolveInstanceMethod: SEL OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);Copy the code

So how do you deal with that? The code!

@interface Test : NSObject
+ (void)test;
- (void)test;
@end

@implementation Test

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(test)) {
        Method method = class_getClassMethod(self, @selector(handleResolveClassMethod));
        class_addMethod(object_getClass(self), sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveClassMethod:sel];
}

+ (void)handleResolveClassMethod {
    NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        Method method = class_getInstanceMethod(self, @selector(handleResolveInstanceMethod));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)handleResolveInstanceMethod {
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Test test];
        [[Test new] test];
    }
    return 0;
}
Copy the code

It is worth noting that class methods exist in metaclasses. So when adding class methods dynamically with the Runtime, remember to find the metaclass of the class to add.

In the case of the add instance method, you simply pass in self.

When we add the corresponding method implementation to the class dynamically. Objc_msgSend will be resent

_lookUpImpTryCache
ALWAYS_INLINE static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior) { runtimeLock.assertUnlocked(); if (slowpath(! cls->isInitialized())) { // see comment in lookUpImpOrForward return lookUpImpOrForward(inst, sel, cls, behavior); } IMP imp = cache_getImp(cls, sel); if (imp ! = NULL) goto done; #if CONFIG_USE_PREOPT_CACHES if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) { imp = cache_getImp(cls->cache.preoptFallbackClass(), sel); } #endif if (slowpath(imp == NULL)) { return lookUpImpOrForward(inst, sel, cls, behavior); } done: if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { return nil; } return imp; }Copy the code

You can see from the method. After the implementation of dynamic method resolution, it is also the process of sending messages again. Start with the method cache list. Go lookUpImpOrForward

At this point, dynamic method parsing is almost complete. If we had not implemented dynamic method resolution, we would have moved to phase 3. forward

forward

Searched the source code. Also can not find the message forward related things. But judging by the call stack that eventually crashed.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[Test test]: unrecognized selector sent to class 0x100008208' *** First throw call stack: ( 0 CoreFoundation 0x00007fff206206af __exceptionPreprocess + 242 1 libobjc.A.dylib 0x00000001002fbb80 objc_exception_throw + 48 2 CoreFoundation 0x00007fff206a2bdd __CFExceptionProem + 0 3 CoreFoundation 0x00007fff2058807d  ___forwarding___ + 1467 4 CoreFoundation 0x00007fff20587a38 _CF_forwarding_prep_0 + 120 5 KCObjcBuild 0x0000000100003ee5 main + 53 6 libdyld.dylib 0x00007fff204c9621 start + 1 )Copy the code

Seeing ___forwarding___, enter the assembly of the call stack. Find that there’s a comment about unrealized methodSignatureForSelector, we try to achieve it.

0x7fff20587fbe <+1276>: leaq   0x5febb1ab(%rip), %rsi    ; @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?"
Copy the code
methodSignatureForSelector
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
Copy the code

You can see that this method needs to return an NSMethodSignature. Looking at the class’s initialization method, we need to pass in the types parameter of the method implementation’s Method_t. TypeEncoding. Let’s try to implement that. Let’s add the following code to the Test class and run it

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}
Copy the code

Error continues after run. Continue to output the call stack.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[Test test]: unrecognized selector sent to class 0x100008258' *** First throw call stack: ( 0 CoreFoundation 0x00007fff206206af __exceptionPreprocess + 242 1 libobjc.A.dylib 0x00000001002fbb80 objc_exception_throw + 48 2 CoreFoundation 0x00007fff206a2bdd __CFExceptionProem + 0 3 libobjc.A.dylib 0x0000000100350957 +[NSObject forwardInvocation:] + 103 4 CoreFoundation 0x00007fff20587e07 ___forwarding___ + 837 5 CoreFoundation 0x00007fff20587a38 _CF_forwarding_prep_0 + 120 6 KCObjcBuild 0x0000000100003ea3 main + 51 7 libdyld.dylib  0x00007fff204c9621 start + 1 )Copy the code
forwardInvocation

You can see that the [NSObject forwardInvocation:] is added.

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}
Copy the code

Go into the source code for NSObject. The doesNotRecognizeSelector method is called. And finally discovered the root of all evil…

Is an unrecognized selector sent to class error output every time a crash occurs

NSInvocation
@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end
Copy the code

We have a breakpoint in the forwardInvocation. Look at what’s in the anInvocation.

(lldb) po anInvocation
<NSInvocation: 0x10070f420>
return value: {v} void
target: {@} 0x100008298
selector: {:} test

(lldb) po 0x100008298
Test
Copy the code

You can see the. AnInvocation has the method name to execute and the target to execute it from. If we call invoke at this point, we will continue to report a method not found error. So how do we solve this. If we let some other class that implements the test method be the target. Let’s try

@interface Test1 : NSObject
@end

@implementation Test1

+ (void)test {
    NSLog(@"%s",__func__);
}

@end

@interface Test : NSObject
+ (void)test;
@end

@implementation Test
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[Test1 class]];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Test test];
    }
    return 0;
}
Copy the code

We found that eventually through message forwarding. Test1 becomes the message receiver and completes the method call. At this point the forwarding of the message ends.