This article Demo portal: MessageForwardingDemo

Abstract: Programming, only understand the principle is not good, must actual combat to know the application scenario. This series tries to explain the theory of Runtime as well as some real-world scenarios, and this is the message forwarding part of the series. In this article, section 1 covers concepts related to method messaging, and section 2 summarizes 2. Dynamic features: Method Resolution, Fast Rorwarding, Normal Forwarding. Section 3 describes several scenarios of Method exchange: Specific crash prevention handling (calling unimplemented methods), apple system iteration causing API incompatible crash handling, and the message forwarding mechanism is summarized in Section 4.

1. Method and message of OC

Before we start using messaging mechanisms, we can agree on our terminology. For example, many people don’t know what “method” and “message” are, but this is critical to understanding how messaging systems work at a low level.

  • methodsThe actual code associated with a class and given a specific name. Ex. :- (int)meaning { return 42; }
  • The message: The name and set of parameters sent to the object. Example: Send to object 0x12345678meaningAnd there are no arguments.
  • The selector: a special way of representing a message or method name, expressed as type SEL. Selectors are essentially opaque strings, and they are managed so that they can be compared using simple pointer equality to increase speed. (Implementations may vary, but this is basically what they look like on the outside.) Such as:@selector(meaning).
  • Message sending: The process of receiving information and finding and executing the appropriate method.

1.1 Methods and Message Sending

Message method invocation in OC is a message sending process. OC methods are eventually generated as C functions with some additional arguments. This C function, objc_msgSend, is responsible for sending the message. Its API can be found in Objc /message.h of Runtime.

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) `Copy the code

1.2 Main steps of Message sending

What happens in the C function when the message is sent? How does the compiler find this method? The main steps for sending a message are as follows:

  1. So let’s first check if we want to ignore this selector. For example, in Mac OS X development, with garbage collection, the functions like retain and release are ignored.
  2. To check if the target of this selector is nil, OC allows us to execute any method on a nil object without crashing, because it’s ignored at runtime.
  3. If you have passed both steps, you will start to look for IMP implementations of the class, first from the cache, and if found, run the corresponding function to execute the corresponding code.
  4. If none is found in the cache, the method list of the class is searched for the corresponding method.
  5. If you can’t find it in the method list of the class, you go to the method list of the superclass, until you find NSObject.
  6. If you still don’t find it, start moving into dynamic method parsing and message forwarding, as described below.

Among them, why is it called a “forward”? An object “forwards” a message when it does not do anything in response to the message. The reason for this is that this technique is primarily for objects to have other objects process messages for them, thus “forwarding.”

Message forwarding is a powerful technique that can greatly increase the expressiveness of Objective-C. What is message forwarding? In short, it allows unknown messages to get stuck and react. In other words, whenever you send an unknown message, it gets sent to your code in a nice package, and you can do whatever you want.

1.3 Method nature of OC

The method in OC has two hidden arguments by default: self and _cmd. You probably know that self is passed as an implicit argument, and it ends up being an explicit argument. The little-known implicit parameter _cmd, which holds the selector for the message being sent, is the second such implicit parameter. Anyway, self points to the object itself, and _cmd points to the method itself. Here are two examples:

  • Example 1: – (NSString *)name This method actually takes two arguments: self and _cmd.

  • Example 2: – (void)setValue:(int)val This method actually takes three arguments: self,_cmd, and val.

The syntax for OC calls you write at compile time will be translated into a C function call objc_msgSend(). For example, the following two lines of code are equivalent:

  • OC
[array insertObject:foo atIndex:5];
Copy the code
  • C
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
Copy the code

Objc_msgSend is responsible for sending messages.

2. Dynamic features: Method parsing and message forwarding

Without the implementation of methods, the program would die at runtime and throw unrecognized selectors sent to… The exception. But before an exception is thrown, the Objective-C runtime gives you three chances to save the application:

  • Method resolution
  • Fast forwarding
  • Normal forwarding

2.1 动态方法解析: Method Resolution

First, the Objective-C runtime calls + (BOOL)resolveInstanceMethod: or + (BOOL)resolveClassMethod:, giving you the opportunity to provide a function implementation. If you add the function and return YES, the runtime system will restart the message sending process once. Using foo as an example, you can do this:

void fooMethod(id obj, SEL _cmd)  
{
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}
Copy the code

Here the first character v represents the return type of the function void, the second character @ represents the type ID of self, and the third character: SEL of _cmd. These symbols can be found in the Developer documentation in Xcode by searching for Type Encodings. The more detailed official document portal is here, which is not listed here.

2.2 Fast forwarding: Fast Rorwarding

Unlike full forwarding below 2.3, Fast Rorwarding is a Fast message forwarding: it is only necessary to return a new object in a designated API method, but other logic judgments are required (such as is this SEL a designated SEL?). .

Before the message forwarding mechanism is implemented, the Runtime system allows us to replace the recipient of the message with another object. Through – (id) forwardingTargetForSelector (SEL) aSelector method. If this method returns nil or self, the message forwarding mechanism (- (void) invocation (NSInvocation *) is entered, otherwise the message is resent to the returned object.

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(foo:)){
        return [[BackupClass alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
Copy the code

2.3 Complete message Forwarding: Normal Forwarding

Different from the above, it can be understood as a complete message forwarding, which can do more than quick forwarding.

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if([alternateObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:alternateObject];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if(! methodSignature) { methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}
Copy the code

ForwardInvocation: A distribution center that doesn’t recognize the message and then forwards it to a different message object, or to the same object, or translates it into something else, or simply “eats” the message so there’s no response and no error. For example, in order to avoid direct flash back, we can give the user a hint when the message can not be processed in this method, which is also a friendly user experience.

Where does the Invocation come from? In forwardInvocation: before the message is sent, the runtime system will send object methodSignatureForSelector: news, and to return to the method signature is used to generate NSInvocation object. So rewrite forwardInvocation: at the same time also to rewrite methodSignatureForSelector: method, otherwise it will throw an exception. When an object cannot respond to a message because there is no corresponding method implementation, the runtime will notify the object via the forwardInvocation: Message. Each object has the forwardInvocation: method, and we can forward the message to another object.

2.4 Differences: Fast Rorwarding versus Normal Forwarding?

Some of your friends may see that both of these forwards are forwarding messages to other objects, so what’s the difference between the two?

  • The API methods that need to be overloaded are used differently

    • The former requires only one API overload, while the latter requires two apis overload.
    • The former simply returns a new object in an API method, while the latter needs to re-sign the forwarded message and manually forward it to the new object (usinginvokeWithTarget:).
  • The number of forwards to new objects is different

    • The former can only forward one object, while the latter can continuously forward multiple objects. For example, here’s the full forward:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector==@selector(run)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector: aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL selector =[anInvocation selector];
    
    RunPerson *RP1=[RunPerson new];
    RunPerson *RP2=[RunPerson new];
    
    if ([RP1 respondsToSelector:selector]) {
        
        [anInvocation invokeWithTarget:RP1];
    }
    if([RP2 respondsToSelector:selector]) { [anInvocation invokeWithTarget:RP2]; }}Copy the code

3. Application: Message forwarding

3.1 Specific collapse prevention and treatment

Here is a code that crashes because there is no implementation method:

  • Test2ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test2ViewController"; / / instantiate a button, did not achieve its methods UIButton * button = [UIButton buttonWithType: UIButtonTypeCustom]; button.frame = CGRectMake(50, 100, 200, 100); button.backgroundColor = [UIColor blueColor]; [buttonsetTitle:@"Message forwarding" forState:UIControlStateNormal];
    [button addTarget:self
               action:@selector(doSomething)
     forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}
Copy the code

To solve this problem, you can create a special category that deals with this problem:

  • NSObject+CrashLogHandle
#import "NSObject+CrashLogHandle.h"@ implementation NSObject (CrashLogHandle) - (NSMethodSignature *) methodSignatureForSelector aSelector: (SEL) {/ / the method signaturereturn [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"NSObject+CrashLogHandle-- this method is not implemented in class :%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}

@end
Copy the code

The following warning appears because the superclass method is copied in the category:

The solution is to ignore all warnings in the resource file in the Build Phases of Xcode with -w at the end of the corresponding file.

3.2 Apple API iteration caused API incompatible crash handling

3.2.1 Traditional solutions for compatible system API iteration

With the annual update iteration of iOS system and hardware, some apis with better performance or better readability may be discarded or replaced. At the same time, we also need to implement version compatibility for old apis in existing apps. Of course, there are many methods for version compatibility, and the author will list several commonly used ones as follows:

  • Judge according to the response method
if ([object respondsToSelector: @selector(selectorName)]) {
    //using new API
} else {
    //using deprecated API
}
Copy the code
  • Determine whether the required classes exist based on the current version of the SDK
if (NSClassFromString(@"ClassName")) {    
    //using new API
}else {
    //using deprecated API
}
Copy the code
  • Determine based on the operating system version
#define isOperatingSystemAtLeastVersion(majorVersion, minorVersion, patchVersion)[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: (NSOperatingSystemVersion) {
    majorVersion,
    minorVersion,
    patchVersion
}]

if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
    //using new API
} else {
    //using deprecated API
}
Copy the code
3.2.2 Compatible new scheme of system API iteration

Requirements: Suppose you have a class that has been written using the new API, as shown below, with one line of code that might crash because it is running on an earlier version of the system (such as iOS9) :

  • Test3ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test3ViewController";
    
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 375, 600) style:UITableViewStylePlain];
    tableView.delegate = self;
    tableView.dataSource = self;
    tableView.backgroundColor = [UIColor orangeColor];
    
    // May Crash Line
    tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
    [self.view addSubview:tableView];
}
Copy the code

There is a line that warns, and Xcode also suggests a solution that automatically adds code to check the system version if you click Fix, as shown below:

Solution 1: Manually add version judgment logic

Previous adaptation can be determined based on the operating system version

if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
    scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
    viewController.automaticallyAdjustsScrollViewInsets = NO;
}
Copy the code

Solution 2: Message forwarding

In iOS11 Base SDK directly adopt the latest API and Runtime message forwarding mechanism can achieve a line of code in different versions of the operating system to take different ways to call messages

  • UIScrollView+Forwarding.m
#import "UIScrollView+Forwarding.h"
#import "NSObject+AdapterViewController.h"

@implementation UIScrollView (Forwarding)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1
    
    NSMethodSignature *signature = nil;
    if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) {
        signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
    }else {
        signature = [super methodSignatureForSelector:aSelector];
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation { // 2
    
    BOOL automaticallyAdjustsScrollViewInsets  = NO;
    UIViewController *topmostViewController = [self cm_topmostViewController];
    NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3
    [viewControllerInvocation setTarget:topmostViewController];
    [viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
    [viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4
    [viewControllerInvocation invokeWithTarget:topmostViewController]; // 5
}

@end
Copy the code
  • NSObject+AdapterViewController.m
#import "NSObject+AdapterViewController.h"

@implementation NSObject (AdapterViewController)

- (UIViewController *)cm_topmostViewController {
    UIViewController *resultVC;
    resultVC = [self cm_topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
    while (resultVC.presentedViewController) {
        resultVC = [self cm_topViewController:resultVC.presentedViewController];
    }
    return resultVC;
}

- (UIViewController *)cm_topViewController:(UIViewController *)vc {
    if ([vc isKindOfClass:[UINavigationController class]]) {
        return [self cm_topViewController:[(UINavigationController *)vc topViewController]];
    } else if ([vc isKindOfClass:[UITabBarController class]]) {
        return [self cm_topViewController:[(UITabBarController *)vc selectedViewController]];
    } else {
        return vc;
    }
}

@end
Copy the code

When we call the new API in iOS10, because there’s no specific API implementation, we forward its original message to the current stack top UIViewController to call the earlier version of the API.

About [the self cm_topmostViewController]; , the result can be viewed as follows:

Overall process of Solution 2:

  1. Returns a corresponding method signature for the message to be forwarded (which is then used to encode the NSInvocation *)

  2. Start message forwarding (NSInvocation *) Encapsulating the original invocation, including method name, method parameters, etc.)

  3. Since the API for forwarding calls is different from the original invocation, here we create a new NSInvocation object viewControllerInvocation for message invocation and configure the corresponding target and selector

  4. Configure the required parameters: Since each method actually comes with two parameters by default :self and _cmd, we start with the third parameter when we want to configure the other parameters

  5. forward

3.2.3 Verifying and Comparing new schemes

Note that when testing, select the iOS10 system simulator for verification (if not, you can Download Simulators), after installation, as follows:

  • Import the UIScrollView+Forwarding class without annotations

  • Comment out the UIScrollView+Forwarding function code

Will crash as shown below:

4. To summarize

4.1 Simulate multiple inheritance

Interview excavation: Does OC support multiple inheritance? Ok, you said you don’t support multiple inheritance, do you have a way to simulate multiple inheritance?

Forward is similar to inheritance and can be used to add some effect of multiple inheritance to OC programming, where an object forwards a message as if it were receiving or “inheriting” a message from another object. Message forwarding makes up for objC’s lack of support for multiple inheritance, and avoids the bloated complexity of a single class caused by multiple inheritance.

Although forwarding can implement inheritance, NSObject has to be superficially rigorous. Methods like respondsToSelector and isKindOfClass only consider inheritance, not forwarding chains.

4.2 Summary of message Mechanism

Sending a message to an object in Objective-C goes through the following steps:

  1. Try to find the message in the Dispatch table of the object class. If found, skip to the corresponding function IMP to execute the implementation code;

  2. If it is not found, Runtime will send +resolveInstanceMethod: or +resolveClassMethod: to try to resolve the message.

  3. If the resolve method to return NO, Runtime is sent – forwardingTargetForSelector: allows you to forward this message to another object.

  4. If there is no new target object returns, the Runtime will send – methodSignatureForSelector: and – forwardInvocation: message. You can send – invokeWithTarget: messages to manually forward or send – doesNotRecognizeSelector: throw an exception.