Run time: message mechanism

Posted by sh0wtym3 on Tue, 25 Feb 2020 13:28:54 +0100

Runtime series

Run time (1): first step
Run time (2): data structure
Run time (3): message mechanism
On the essence of Runtime (4): super
Run time (5): specific application
Run time (6): related interview questions

1. Call process of objc_msgsend method

  • The method calls in OC are actually all the calls converted to objc [msgsend() function (excluding [super message]).
void objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
  • Send a message (SEL method name) to the receiver (method caller / message receiver)
    Parameter 1: receiver
    Parameter 2: sel
    Parameters 3, 4, 5 : parameters of SEL method
  • The execution process of objc ﹣ msgsend() can be divided into three stages:
    message sending
    Dynamic method analysis
    Message forwarding

 

2. Message sending

Message sending process


Source code analysis

As mentioned in the previous article, Runtime is a Runtime library written in C and assembly,
If the C function needs to be called in the underlying assembly, Apple will underline it,
Therefore, to view the implementation of objc ﹣ msgsend function, you need to search for ﹣ objc ﹣ msgsend (objc-msg-arm64.s (objc4)).

// objc-msg-arm64.s(objc4)
    /*
       _objc_msgSend Function realization
    */
    // A kind of Assembler ENTRY format: ENTRY + function name
	ENTRY _objc_msgSend

    // A kind of If the receiver is nil or tagged pointer, execute LNilOrTagged, otherwise continue to execute
	cmp	x0, #0			// nil check and tagged pointer check
	b.le	LNilOrTagged   

    // A kind of Find class / meta class through isa
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
    // A kind of Enter the cache to search, and the parameter passed is NORMAL
    // CacheLookup macro, used to find SEL corresponding method implementation in cache
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached 

LNilOrTagged:
    // A kind of If the receiver is nil, run LReturnZero to end objc ﹐ msgsend
	b.eq	LReturnZero		// nil check 
    // A kind of If the receiver is tagged pointer, execute other
    ......
	b	LGetIsaDone

LReturnZero:
    ret  // Return

    // A kind of In assembly, the end format of the function is: ENTRY + function name
	END_ENTRY _objc_msgSend



.macro CacheLookup
    // A kind of - search methods in hash table buckets according to SEL
	// x1 = SEL, x16 = isa
	ldp	x10, x11, [x16, #CACHE]	// x10 = buckets, x11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	x12, x10, x12, LSL #4	// x12 = buckets + ((_cmd & mask)<<4)

	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
    // A kind of - cache hit, cache hit
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
    // A kind of No cache found, CheckMiss operation
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]!	// {x9, x17} = *--bucket
	b	1b			// loop
3:	// wrap: x12 = first bucket, w11 = mask
	add	x12, x12, w11, UXTW #4	// x12 = buckets+(mask<<4)
	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.
	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]!	// {x9, x17} = *--bucket
	b	1b			// loop
3:	// double wrap
	JumpMiss $0	
.endmacro


// CacheLookup NORMAL|GETIMP|LOOKUP
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheHit
.if $0 == NORMAL  // A kind of The parameter of CacheLookup is NORMAL
	MESSENGER_END_FAST
	br	x17			// call imp  //  A kind of Execution function
.elseif $0 == GETIMP
	mov	x0, x17			// return imp
	ret
.elseif $0 == LOOKUP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro


.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	x9, LGetImpMiss
.elseif $0 == NORMAL  // A kind of The parameter of CacheLookup is NORMAL
	cbz	x9, __objc_msgSend_uncached  // A kind of Run objc msgsend uncached
.elseif $0 == LOOKUP
	cbz	x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro


.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro



    // ⚠️__objc_msgSend_uncached
    // A kind of There is no method implementation found in the cache. Next, go to the method list of MethodTableLookup class to find
	STATIC_ENTRY __objc_msgSend_uncached
	MethodTableLookup NORMAL
	END_ENTRY __objc_msgSend_uncached

.macro MethodTableLookup
	blx	__class_lookupMethodAndLoadCache3  // A kind of Run the C function, class, lookupmethodandloadcache3
.endmacro

On the contrary, when finding the corresponding C function through the function name in the assembly, you need to remove an underscore.

// objc-runtime-new.mm(objc4)
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    // A kind of Please pay attention to the parameters. Since the assembly has been used to search in the cache before, we will not search in the cache again
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;  // triedResolver tag for dynamic method parsing

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {  // cache = NO, skip 
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search 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.

    runtimeLock.read();

    if (!cls->isRealized()) {  // A kind of If the receiverclass has not been implemented, perform the realize operation
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    // A kind of If the receiverClass needs to be initialized and has not yet been initialized, initialize it
    // Insert a knowledge point of + initialize method here
    // Call ﹣ class ﹣ initialize (CLS), which will recursively traverse the parent class to determine whether the parent class exists and has not been initialized ﹣ class ﹣ initialize (CLS - > superclass)
    // Call call initialize (cls) and send an initialize message ((void (*) (class, SEL)) objc ﹣ msgsend) (cls, sel ﹣ initialize) to cls
    // So the + initialize method is called the first time the class receives a message
    // Call method: objc_msgSend()
    // Call order: first call + initialize of the parent class, then + initialize of the child class (first initialize the parent class, then the child class, each class will only be initialized once)
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }


// A kind of A kind of A kind of A kind of A kind of Core 
 retry:    
    runtimeLock.assertReading();

    // A kind of Go to the cache of receiverClass to find the method. If imp is found, call it directly
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // A kind of Go to the method list in receiverClass's class \ RW \ t to find the method. If imp is found, call it and cache the method in receiverClass's cache
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);  // A kind of Go to the method list of the target class to find the method implementation
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);  // A kind of Caching method
            imp = meth->imp;
            goto done;
        }
    }

    // A kind of Level by level search the cache and method list of the parent class. If imp is found, call and cache the method in the cache of receiverClass
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    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.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }


    // A kind of Enter the stage of "dynamic method analysis"
    // No implementation found. Try method resolver once.
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }


    // A kind of Enter the stage of "message forwarding"
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);


 done:
    runtimeLock.unlockRead();

    return imp;
}

Let's take a look at how getmethodnoseuper_nolock (CLS, SEL) looks up a method implementation from a class

  • If the method list is sorted, binary search is performed;
  • If the method list is not sorted, a linear traversal lookup is performed.
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
    // A kind of Core function search method list ()
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}


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)) {
        // A kind of If the method list is sorted, perform binary search
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // A kind of If the method list is not sorted, perform a linear traversal search
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    ......
    return nil;
}


static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // A kind of : count > > = 1 binary search
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

Let's take a look at how log and fill cache (CLS, meth->imp, SEL, Inst, CLS) caches methods

/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled. 
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

#if TARGET_OS_WIN32  ||  TARGET_OS_EMBEDDED
#   define SUPPORT_MESSAGE_LOGGING 0
#else
#   define SUPPORT_MESSAGE_LOGGING 1
#endif

The implementation of the cache_fill() function has been written in the previous article.
For the cache lookup process and more information about cache_t, you can view:
Runtime (two): data structure

 

2. Dynamic method analysis

"Dynamic method analysis" process

  • If the implementation of the method is not found in the "message sending" phase, a "dynamic method parsing" is performed;
  • After "dynamic method parsing", it will enter the "message sending" process again,
    Start from the step of "find method in cache of receiverClass".
  • We can override the following methods based on the method type (instance method or class method)
    +(BOOL)resolveInstanceMethod:(SEL)sel
    +(BOOL)resolveClassMethod:(SEL)sel
    The following methods are invoked in the method to dynamically add the implementation of the method.
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  • The example code is as follows. We call the eat instance method and class method of HTPerson respectively, but there is no corresponding implementation of these two methods in HTPerson.m file. We dynamically add the implementation for these two methods, and the output results are as follows.
// main.m
#import <Foundation/Foundation.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[HTPerson new] eat];
        [HTPerson eat];   
    }
    return 0;
}
@end

// HTPerson.h
#import <Foundation/Foundation.h>
@interface HTPerson : NSObject
- (void)eat;  // No corresponding implementation
- (void)sleep;
+ (void)eat;  // No corresponding implementation
+ (void)sleep;
@end

// HTPerson.m
#import "HTPerson.h"
#import <objc/runtime.h>
@implementation HTPerson
- (void)sleep
{
    NSLog(@"%s",__func__);
}
+ (void)sleep
{
    NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        
        // Get other methods. Method is the pointer to the structure of method
        Method method = class_getInstanceMethod(self, @selector(sleep));
        /*
         Parameter 1: which class to add
         Parameter 2: which method to add
         Parameter 3: implementation address of the method
         Parameter 4: encoding type of method
         */
        class_addMethod(self,  // Instance methods are stored in class objects, so class objects are passed in here
                        sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method)
                        );
        // Return YES to represent the implementation of dynamic adding method
        // From the source code point of view, this return value is only used to print the information related to parsing results, and does not affect the results of dynamic method parsing
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        
        Method method = class_getClassMethod(object_getClass(self), @selector(sleep));

        class_addMethod(object_getClass(self),  // Class methods are stored in metaclass objects, so metaclass objects are passed in here
                        sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method)
                        );
        return YES;
    }
    return [super resolveClassMethod:sel];
}

@end

-[HTPerson sleep]
+[HTPerson sleep]

Source code analysis

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;
    ......   

 retry:    
    ......

    // A kind of If the method implementation is not found in the "message sending" phase, perform a "dynamic method parsing"
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);  // A kind of Core function
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;  // A kind of Mark triedResolver as YES
        goto retry;  // A kind of Go to message sending again, and start from the step of "search method in cache of receiverClass"
    }

    // A kind of Enter the stage of "message forwarding"
    ......
}

// objc-class.mm(objc4)
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    // A kind of Determine whether it is a class object or a meta class object
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        // A kind of Core function
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        // A kind of Core function
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // A kind of Check whether the method list of the receiverClass's meta class object contains the SEL? Resolveinstancemethod function imp
    // A kind of That is to see if we have implemented the + (BOOL)resolveInstanceMethod:(SEL)sel method
    // A kind of This method must be found here, because there is an implementation in NSObject
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // A kind of If it is not found, it indicates that the program is abnormal and returns directly
        // Resolver not implemented.
        return;
    }

    // A kind of If found, send a sel resolve instancemethod message to the object through objc > msgsend
    // A kind of Call + (BOOL)resolveInstanceMethod:(SEL)sel method
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // A kind of Here are some printouts of the results
    ......
}

/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    // A kind of Check the method list of the receiverClass's meta class object to see if there is sel ﹐ resolveclassmethod function imp
    // A kind of That is to see if we have implemented the + (BOOL)resolveClassMethod:(SEL)sel method
    // A kind of This method must be found here, because there is an implementation in NSObject
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // A kind of If it is not found, it indicates that the program is abnormal and returns directly
        // Resolver not implemented.
        return;
    }
    // A kind of If found, send a sel resolve class method message to the object through objc \ msgsend
    // A kind of Call + (BOOL)resolveClassMethod:(SEL)sel method
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst),   // The return value of this function is a class object, not a metaclass object
                        SEL_resolveClassMethod, sel);

    // A kind of Here are some printed information of the analysis results
    ......
}

 

3. Message forwarding

Message forwarding process

  • If the implementation of the method is not found in the "message sending" phase, and it is not solved through "dynamic method parsing",
    Enter the "message forwarding" stage;
  • The "message forwarding" stage is divided into two steps: Fast forwarding and Normal forwarding. As the name suggests, the first step is faster than the second step;
  • Fast forwarding: forward a message to another OC object (find an alternate receiver),
    We can override the following methods to return an object with! = receiver to complete this step;
    +/- (id)forwardingTargetForSelector:(SEL)sel
  • Normal forwarding: implement a complete message forwarding process,
    If the previous step fails to solve the unknown message, you can override the following two methods to start the complete message forwarding.
     
    ① The first method: we need to return a method signature suitable for the unknown message in the method (method signature is a description of the return value Type and parameter Type, which can be encoded with Type Encodings. For Type Encodings, please read my previous blog Runtime (two): data structure).
    +/- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    Runtime will sign according to this method and create a NSInvocation object (NSInvocation encapsulates all the contents of the unknown message, including: the method caller target, the method name selector, the method parameter argument, etc.), then calls the second method and imports the NSInvocation pair image as the parameter.
     
    ② The second method: we can forward the unknown message to other objects, change the content of the unknown message (such as method name, method parameter) and forward it to other objects, or even define any logic.
    +/- (void)forwardInvocation:(NSInvocation *)invocation
    If there is no return method signature in the first method, or if we do not override the second method, the system will think that we do not want to process the message at all. At this time, the system will call the + / - (void)doesNotRecognizeSelector:(SEL)sel method and throw the classic crash: unrecognized selector send to instance / class to end the whole process of objc_msgSend.
     
  • Let's take a look at the default implementation of these codes
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}
+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}
  • The Fast forwarding example code is as follows:
    We call the eat instance method of HTPerson, but there is no corresponding implementation of the method in HTPerson.m file. HTDog.m has the implementation of the method with the same name. We forward the message to the instance object of HTDog, and the output result is as follows.
// main.m
#import <Foundation/Foundation.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[HTPerson new] eat];        
    }
    return 0;
}
@end

// HTPerson.h
#import <Foundation/Foundation.h>
@interface HTPerson : NSObject
- (void)eat;  // No corresponding implementation
@end

// HTPerson.m
#import "HTPerson.h"
#import "HTDog.h"
@implementation HTPerson
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat)) {
        return [HTDog new];  // Forward the eat message to the instance object of HTDog
//        return [HTDog class]; / / you can also forward the eat message to the HTDog class object
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

// HTDog.m
#import "HTDog.h"
@implementation HTDog
- (void)eat
{
    NSLog(@"%s",__func__);
}
+ (void)eat
{
    NSLog(@"%s",__func__);
}
@end

-[HTDog eat]

  • The Normal forwarding example code and output results are as follows:
// HTPerson.m
#import "HTPerson.h"
#import "HTDog.h"

@implementation HTPerson

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat)) {
        
        return [[HTDog new] methodSignatureForSelector:aSelector];
        //return [NSMethodSignature signatureWithObjCTypes:"v@i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // Forward unknown messages to other objects
    [anInvocation invokeWithTarget:[HTDog new]];
    
    // Change the content of the unknown message (such as method name, method parameter) and forward it to other objects
    /*
    anInvocation.selector = @selector(sleep);
    anInvocation.target = [HTDog new];
    int age;
    [anInvocation getArgument:&age atIndex:2];  // Parameter order: target, selector, other arguments
    [anInvocation setArgument:&age atIndex:2];  // The number of parameters is determined by the signature of the method returned by the previous method. Pay attention to the problem of array overrun
    [anInvocation invoke];
    
    int ret;
    [anInvocation getReturnValue:&age];  // Get return value
     */
    
    // Define any logic, such as printing only one sentence
    /*
     NSLog(@"Study hard);
     */
}

@end

-[HTDog eat]

Source code analysis

// objc-runtime-new.mm(objc4)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ......

    // A kind of If the implementation of the method is not found in the "message sending" phase, and it is not solved through "dynamic method parsing"
    // A kind of Enter the stage of "message forwarding"
    imp = (IMP)_objc_msgForward_impcache;  // Enter assembler
    cache_fill(cls, sel, imp, inst);       // Cache method

    ......
}
// objc-msg-arm64.s(objc4)
	STATIC_ENTRY __objc_msgForward_impcache
	b	__objc_msgForward
	END_ENTRY __objc_msgForward_impcache
	
	ENTRY __objc_msgForward
	adrp	x17, __objc_forward_handler@PAGE   // A kind of Run C function objc forward handler
	ldr	x17, [x17, __objc_forward_handler@PAGEOFF]
	br	x17
	END_ENTRY __objc_msgForward
// objc-runtime.mm(objc4)
// Default forward handler halts the process.
__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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

You can see that objc forward handler is a function pointer to objc defaultforwardhandler(), which just prints information. As apple has not opened this source, we can no longer explore the detailed execution logic of "message forwarding".

We know that if we call a method that is not implemented, and there is no "dynamic method parsing" and "message forwarding" processing, it will report the classic crash: unrecognized selector send to instance / class. Let's look at the function call stack of crash printing information. As follows, the system you can see calls a function called forwarding.

This function is in the core foundation framework. Apple has not yet opened the source of this function. We can break into the assembly implementation of this function.

The following is the pseudo code implementation of C language found on the Internet.

// Pseudo code
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // A kind of A kind of A kind of A kind of A kind of Call forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        // A kind of Determine whether the method returns an object and the object! = receiver
        if (forwardingTarget && forwardingTarget != receiver) {
            if (isStret == 1) {
                int ret;
                objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
                return ret;
            }
            //A kind of - objc uumsgsend (return value, sel,...);
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // Zombie object
    const char *className = class_getName(receiverClass);
    const char *zombiePrefix = "_NSZombie_";
    size_t prefixLen = strlen(zombiePrefix); // 0xa
    if (strncmp(className, zombiePrefix, prefixLen) == 0) {
        CFLog(kCFLogLevelError,
              @"*** -[%s %s]: message sent to deallocated instance %p",
              className + prefixLen,
              selName,
              receiver);
        <breakpoint-interrupt>
    }

    // A kind of A kind of A kind of A kind of A kind of Call methodSignatureForSelector to get method signature, and then call forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        // A kind of Call methodSignatureForSelector to get method signature
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        // A kind of Determine whether the return value is nil
        if (methodSignature) {
            BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
            if (signatureIsStret != isStret) {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
                      selName,
                      signatureIsStret ? "" : not,
                      isStret ? "" : not);
            }
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {

                // A kind of Create an NSInvocation object based on the method signature
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

                // A kind of Call forwardInvocation
                [receiver forwardInvocation:invocation];

                void *returnValue = NULL;
                [invocation getReturnValue:&value];
                return returnValue;
            } else {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
                      receiver,
                      className);
                return 0;
            }
        }
    }

    SEL *registeredSel = sel_getUid(selName);

    // Whether the selector has been registered in the Runtime
    if (sel != registeredSel) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
              sel,
              selName,
              registeredSel);
    } // A kind of A kind of A kind of A kind of A kind of Call dosnotrecognizeselector 
    else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {

        [receiver doesNotRecognizeSelector:sel];
    }
    else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
              receiver,
              className);
    }

    // The point of no return.
    kill(getpid(), 9);
}

 

summary

At this point, the objc UU msgsend method call process is finished. Let's make a small summary.

Objc & msgsend execution flow chart

Published 8 original articles, praised 0, visited 79
Private letter follow

Topics: encoding C