This article will explain the principle and application scenarios of message forwarding mechanism

1. Principle of message forwarding mechanism

To understand the message forwarding mechanism, it is important to understand the meanings and differences of a few key words: method, selector, and message

  • Test is a function that returns no parameters. Test is a function that returns no parameters.
- (void)test{
    NSLog(@"this is a method without params");
}
Copy the code
  • Selector: A selector is actually a special string representing a function, defined as SEL, that is usually hidden from visual access. For example, I usually write@selector(test), this is the SEL method to get the test function.
  • Messages: In fact, OC functions written by us will eventually be compiled into C language functions, OC function calls will also be converted into C language objc_msgSend function calls, the parameters of the objc_msgSend function can be understood as messages, the process of function calls can be understood as message forwarding. Objc_msgSend (Forwarding (ID), selector (SEL), parameter, parameter…)

1. Message forwarding process (call process of OC function)

The following diagram shows the process of message forwarding:

Common mistakes we make:unrecognized selector sent to instance 0x7fd719006f40(3) Failed to find a pair of instances or class objects that match the methoddoesNotRecognizeSelectorThrow an exception. Let’s talk more about this process.

******** The following content is based on the above flow chart, so the following content please combine with this picture to understand

1.1 Method Search

This is actually the process on the left side of the figure above, where when you make a method call, if it’s an instance method call, you loop through the list of instance methods of that class, looking for the parent class if you don’t find it, all the way to the root class NSObject; In the case of a class method call, the method list of the class’s metaclass is first iterated, then the parent metaclass is searched, and finally the root metaclass. If the corresponding method can be found in the above process, the process ends; if no corresponding method can be found, the process on the right enters.

1.2 Message Forwarding

If you enter the flow on the right, it means that the method call is currently in the exception handling phase. This phase has three small phases, meaning three opportunities to handle the exception.

1.2.1 Method Resolution: resolveInstanceMethod, resolveClassMethod

These two functions handle instance object method calls and class method calls respectively. To demonstrate this, we’ll create a new class fromView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface FromView : UIView

@end

NS_ASSUME_NONNULL_END
Copy the code

FromView.m

#import "FromView.h"

@implementation FromView

@end
Copy the code

Next we call the following methods. You can see the difference between calling a method using performSelector and calling a method directly here

FromView *fromView = [[FromView alloc]init];
[fromView performSelector:@selector(instanceMethodTest) withObject:@"instanceMethodTest"];
[FromView performSelector:@selector(classMethodTest) withObject:@"classMethodTest"]
Copy the code

After running the above code, a familiar error is reported: unrecognized Selector sent to instance 0x7ff084705620. Next we implement the message forwarding process ① by adding the following code to the fromView. m file

+(BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"instanceMethodTest:"]) {
        Method method = class_getInstanceMethod([self class], @selector(addDynamicInstanceMethod:));
        IMP methodIMP = method_getImplementation(method);
        const char * types = method_getTypeEncoding(method);
        class_addMethod([self class].sel.methodIMP.types);
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+(BOOL)resolveClassMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"classMethodTest:"]) {
        // Class methods exist in the metaclass, so adding methods needs to be added to the metaclass
        Class metaClass = object_getClass([self class]);
        Method method = class_getClassMethod([self class], @selector(addDynamicClassMethod:));
        IMP methodIMP = method_getImplementation(method);
        const char * types = method_getTypeEncoding(method);
        class_addMethod(metaClass, sel, methodIMP, types);
        return YES;
    }
    return [superresolveClassMethod:sel]; } - (void)addDynamicInstanceMethod:(NSString *)value {
    NSLog(@"addDynamicInstanceMethod value = %@",value); } + (void)addDynamicClassMethod:(NSString *)value {
    NSLog(@"addDynamicClassMethod value = %@",value);
}
Copy the code

We use Runtime to dynamically add two methods to resolve to run successfully and output results.

Output result:2021-11-11 16:47:20.553244+0800 testClass[25034:6211939] addDynamicInstanceMethod value = instanceMethodTest
2021-11-11 16:47:20.553418+0800 testClass[25034:6211939] addDynamicClassMethod value = classMethodTest
Copy the code

In resolve, if No is returned, flow ②, which is a quick forward process, is entered. If it returns yes, then the compiler will resend the message, and then essentially call performSelector again. Because we have dynamically added instance and class methods through the resolve function, resending the message will respond normally.

1.2.2 Fast forwarding: Fast Rorwarding

As before, the unrecognized selector sent to instance error occurs because the resolve phase does not handle the two methods

[fromView performSelector:@selector(instanceMethodTestFastForwarding:) withObject:@"instanceMethodTestFastForwarding"];
[FromView performSelector:@selector(classMethodTestFastForwarding:) withObject:@"classMethodTestFastForwarding"];
Copy the code

The main thing that the Fast Rorwarding process needs to do is tell the compiler which instance object or class object is currently able to handle the method. We’ll create a new class that implements the two methods we just called, but there are no other properties and methods in the subFromView.h file, so we won’t list them

#import "SubFromView.h"

@implementation SubFromView

-(void)instanceMethodTestFastForwarding:(NSString *)strValue {
    NSLog(@"instanceMethodTestFastForwarding value=%@",strValue); } + (void)classMethodTestFastForwarding:(NSString *)strValue {
    NSLog(@"classMethodTestFastForwarding value=%@",strValue);
}

@end
Copy the code

Add a method to implement fast forwarding in fromView. m

-(id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"instanceMethodTestFastForwarding:"]) {
        SubFromView * subFromView = [[SubFromView alloc]init];
        if ([subFromView respondsToSelector:aSelector]) {
            returnsubFromView; }}return [super forwardingTargetForSelector:aSelector];
}

+(id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"classMethodTestFastForwarding:"]) {
        if ([SubFromView respondsToSelector:aSelector]) {
            return  [SubFromView class]; }}return [super forwardingTargetForSelector:aSelector];
}
Copy the code

When we run it again, we can see that our new SubFromView finally calls both methods, and that the message is finally forwarded to the SubFromView.

2021-11-11 16:47:20.553244+0800 testClass[25034:6211939] addDynamicInstanceMethod value = instanceMethodTest
2021-11-11 16:47:20.553418+0800 testClass[25034:6211939] addDynamicClassMethod value = classMethodTest
2021-11-11 16:47:20.553677+0800 testClass[25034:6211939] instanceMethodTestFastForwarding value=instanceMethodTestFastForwarding
2021-11-11 16:47:20.553823+0800 testClass[25034:6211939] classMethodTestFastForwarding value=classMethodTestFastForwarding
Copy the code

ForwardingTargetForSelector this function mainly is the need to return should be a phase method of the object, can be a class object or instance objects, as long as it can response the method. If you return self or nil then you go to process ③, which is the full message forward phase

1.2.3 Complete Message Forwarding: Normal Forwarding

Then we call the following function, which, as before, still generates an unrecognized selector sent to instance error, because these two methods cannot be handled in the fast forwarding stage, and then enters the process ③, complete message forwarding

[fromView performSelector:@selector(instanceMethodTestNormalForwarding:) withObject:@"instanceMethodTestNormalForwarding"];
[FromView performSelector:@selector(classMethodTestNormalForwarding:) withObject:@"classMethodTestNormalForwarding"];
Copy the code

Now we’ll create a new class FromNSObject(inherited FromNSObject, with no additional properties or methods) and add the implementations of the two functions called from FromNSObject and SubFromView

- (void)instanceMethodTestNormalForwarding:(NSString *)strValue {
    NSLog(@"instanceMethodTestNormalForwarding value=%@",strValue); } + (void)classMethodTestNormalForwarding:(NSString *)strValue {
    NSLog(@"classMethodTestNormalForwarding value=%@",strValue);
}
Copy the code

Then add the following code to fromView.m to implement Normal Forwarding

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = anInvocation.selector;
    BOOL found = FALSE;
    if ([NSStringFromSelector(selector) isEqualToString:@"instanceMethodTestNormalForwarding:"]) {
        SubFromView * subFromView = [[SubFromView alloc]init];
        FromNSObject * fromNSObject = [[FromNSObject alloc]init];
        if ([subFromView respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:subFromView];
            found = YES;
        }
        if ([fromNSObject respondsToSelector:selector]){
            [anInvocation invokeWithTarget:fromNSObject];
            found = YES;
        }
        
        // optional
        if(! found) { [self doesNotRecognizeSelector:selector]; } } } -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature * sign = [super methodSignatureForSelector:aSelector];
    if(! sign) { sign = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    returnsign; } + (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = anInvocation.selector;
    BOOL found = FALSE;
    if ([NSStringFromSelector(selector) isEqualToString:@"classMethodTestNormalForwarding:"]) {
        if ([SubFromView respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:[SubFromView class]];
            found = YES;
        }
        if ([FromNSObject respondsToSelector:selector]){
            [anInvocation invokeWithTarget:[FromNSObject class]];
            found = YES;
        }
        
        // optional
        if(! found) { [self doesNotRecognizeSelector:selector]; } } } +(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature * sign = [super methodSignatureForSelector:aSelector];
    if(! sign) { sign = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return sign;
}
Copy the code
Output result:2021-11-11 17:53:29.823458+0800 testClass[26049:6266557] addDynamicInstanceMethod value = instanceMethodTest
2021-11-11 17:53:29.823565+0800 testClass[26049:6266557] addDynamicClassMethod value = classMethodTest
2021-11-11 17:53:29.823736+0800 testClass[26049:6266557] instanceMethodTestFastForwarding value=instanceMethodTestFastForwarding
2021-11-11 17:53:29.823860+0800 testClass[26049:6266557] classMethodTestFastForwarding value=classMethodTestFastForwarding
2021-11-11 17:53:29.824170+0800 testClass[26049:6266557] instanceMethodTestNormalForwarding value=instanceMethodTestNormalForwarding
2021-11-11 17:53:29.824272+0800 testClass[26049:6266557] instanceMethodTestNormalForwarding value=instanceMethodTestNormalForwarding
2021-11-11 17:53:29.824425+0800 testClass[26049:6266557] classMethodTestNormalForwarding value=classMethodTestNormalForwarding
2021-11-11 17:53:29.824513+0800 testClass[26049:6266557] classMethodTestNormalForwarding value=classMethodTestNormalForwarding
Copy the code

Process three need to implement methodSignatureForSelector and forwardInvocation two functions at the same time, quite so to give the message to sign, and then call forwardInvocation forward. [NSMethodSignature signatureWithObjCTypes:”v@:@”]; “V @:@” is apple’s official type definition, please refer to the official documentation for details

Since both flow ② and flow ③ are forwarding to other objects, what is the difference between flow ② and flow ③?

1.2.4 Differences between Normal Forwarding and Fast Rorwarding:

  1. Flow 2 can forward a maximum of one object, but flow 3 can forward multiple objects at the same time.
  2. Process (2) only need to implement forwardingTargetForSelector this method, but the process (3) must realize methodSignatureForSelector and forwardInvocation method at the same time.
  3. The flow ② must specify the forwarding object or enter the flow ③, but as the final step, the flow ③ can not specify the forwarding object, and it can also depend on whether the mood wants to call doesNotRecognizeSelector to control throwing exceptions. Therefore, in fact, process ③ can be used to avoid flash back. For example, when no forwarding object is found, a friendly error message is displayed instead of flash back directly, which can greatly optimize user experience.

2. Application scenarios of message forwarding mechanism

After all this, many people may wonder what this message forwarding mechanism is for. Forwarding of three phases, in order to avoid flash back, so we want to realize the three stages at any stage, we are all need to know first abnormal method name and argument types, since we already know this information, but why did not call the class add good these instance methods and class methods, also need so tedious to write such a pile of handling exceptions. I felt the same way before I knew about message forwarding, and it was lame. Uh… In fact, NOW I also feel a little chicken ribs, ha ha, probably the realm is not to use such high-end things.

Okay, let’s get down to business and start by listing how others have made good use of this message forwarding mechanism. The following is a reference to iOS Development runtime principles and practices

2.1 Prevent specific crashes

As I mentioned earlier when I talked about process 3, process 3 can ignore the error completely without handling the message forward or throwing an exception. So we can create a new Category of NSObject, and we can rewrite the two functions in flow ③, so that we don’t have to blink because of this function call error, and we can pop up the information about this exception, and it’s kind of tester friendly when we’re testing our APP, or at least it doesn’t tell you that we’re blinking again, Prompts can also help fix bugs. It is recommended to use this method with caution. After all, it is the root class that is rewritten. If the message forwarding process is processed in the subclass, there will be surprises

#import "NSObject+Resolve.h"

@implementation NSObject (Resolve)

+(BOOL)resolveInstanceMethod:(SEL)sel{
    return NO;
}

-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self;
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    returnsign; } - (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"something wrong,method is %@",NSStringFromSelector(anInvocation.selector));
}

@end
Copy the code

2.2 Compatibility between system methods of different versions

The simplest example is this

tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
Copy the code

As for the way to implement it, I suggest checking iOS Development · Runtime principles and practices. It is clearly written in section 3.2.1 of the article catalog. I have never done this before.

2.3 Implementing multiple inheritance

First of all, iOS does not support multiple inheritance. I guess apple thinks multiple inheritance with ambiguity is not rigorous, so it makes swift so rigorous that Int+Double is not allowed. Multiple inheritance is a mixed blessing. The downside is the ambiguity I just mentioned, which is the biggest problem, but the advantage is also obvious. We can inherit multiple parent classes at the same time and obtain the public properties and methods of these classes.

Demo code listed below, the parent class have Father and Mother and child as the Son, the Son, m by forwardingTargetForSelector message forwarding, indirect implements multiple inheritance.

// Father.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol FatherProtocol<NSObject>

-(void)fatherMethod;

@end

@interface Father : NSObject<FatherProtocol>

@end
NS_ASSUME_NONNULL_END
// Father.m

#import "Father.h"

@implementation Father

- (void)fatherMethod{
    NSLog(@"this is fatherClass");
}

@end
------------------------------------------------------
// Mother.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol MotherProtocol<NSObject>

-(void)motherMethod;

@end

@interface Mother : NSObject<MotherProtocol>

@end

NS_ASSUME_NONNULL_END
// Mother.m
#import "Mother.h"

@implementation Mother

- (void)motherMethod {
    NSLog(@"this is motherClass");
}

@end
---------------------------------------------------
// Son.h
#import <Foundation/Foundation.h>
#import "Mother.h"
#import "Father.h"

NS_ASSUME_NONNULL_BEGIN

@interface Son : NSObject

@property (nonatomic,strong) Mother * mother;
@property (nonatomic,strong) Father * father;

@end
// Son.m
#import "Son.h"

@implementation Son

- (instancetype)init
{
    self = [super init];
    if (self) {
        _mother = [[Mother alloc]init];
        _father = [[Father alloc]init];
    }
    return self;
}

+(BOOL)resolveInstanceMethod:(SEL)sel {
    return [super resolveInstanceMethod:sel];
}

-(id)forwardingTargetForSelector:(SEL)aSelector {
    if ([_mother respondsToSelector:aSelector]) {
        return _mother;
    }
    if ([_father respondsToSelector:aSelector]) {
        return _father;
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

NS_ASSUME_NONNULL_END

/ * * *, of course, I feel a different way to write as if also realized the multiple inheritance, and easier to understand, as shown in the following: * /
// Son.h
#import <Foundation/Foundation.h>
#import "Mother.h"
#import "Father.h"

NS_ASSUME_NONNULL_BEGIN

@interface Son : NSObject<MotherProtocol,FatherProtocol>

@property (nonatomic,strong) Mother * mother;
@property (nonatomic,strong) Father * father;

@end

NS_ASSUME_NONNULL_END

// Son.m
#import "Son.h"

@implementation Son

- (instancetype)init
{
    self = [super init];
    if (self) {
        _mother = [[Mother alloc]init];
        _father = [[Father alloc]init];
    }
    return self;
}

- (void)motherMethod {
    if([_mother respondsToSelector:@selector(motherMethod)]) { [_mother motherMethod]; }} - (void)fatherMethod {
    if ([_father respondsToSelector:@selector(fatherMethod)]) {
        [_father fatherMethod];
    }
}

@end
Copy the code

Reference article:

  • IOS development · Runtime principles and practices
  • PerformSelector for message processing
  • Mechanism and application of the iOS Runtime message forwarding mechanism