iOS Development runtime Principle and Practice: Message Forwarding (Message mechanism, method not implemented + API incompatible rush, simulate multiple inheritance)

Keywords: iOS xcode Programming SDK

This article Demo portal: RuntimeDemo

ABSTRACT: Programming can not only understand the principle, but also know the application scenario in practice. This series tries to elaborate the relevant theory of runtime and introduce some real battlefield scenarios. This article is the message forwarding chapter of this series. In this article, the first section will introduce the concepts related to method messaging. The second section will summarize 2. Dynamic characteristics: Method Resolution, Fast Rorwarding, Normal Forwarding. The third section will introduce several scenarios of method exchange: specific rush prevention processing (invoking unrealized methods), API incompatible rush caused by Apple system iteration. The fourth section summarizes the mechanism of message forwarding.

1.OC Method and Message

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

  • Method: An actual code related to a class is given with a specific name. Example: - (int) meaning {return 42;}
  • Message: The name and a set of parameters sent to the object. Example: Send meaning to 0x12345678 object with no parameters.
  • Selector: A special way to represent a message or method name, expressed as a type SEL. Selectors are essentially opaque strings, and they are managed, so they can be compared using simple pointer equivalence to increase speed. (Implementation may vary, but it's basically what they look like outside.) For example: @selector(meaning).
  • Message sending: The process of receiving information and finding and executing appropriate methods.

1.1 Method and Message Sending

Method invocation of messages in OC is a process of sending messages. The OC method is eventually generated as a C function with some additional parameters. The C function objc_msgSend is responsible for sending messages. Its API can be found in runtime's objc/message.h.

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

The main steps of 1.2 message sending

What happened in the C language function when the message was sent? How did the compiler find this method? The main steps of message sending are as follows:

  1. First check if the selector is to be ignored. For example, in the development of Mac OS X, with garbage collection, we don't care about retain and release functions.
  2. Detecting whether the target of this selector is nil, OC allows us to execute any method on a nil object without Crash, because the runtime will be ignored.
  3. If the above two steps are passed, I will start to find the IMP implementation of this class, first from the cache, if found, run the corresponding function to execute the corresponding code.
  4. If it is not found in the cache, find out if there are corresponding methods in the method list of the class.
  5. If the method list of the class cannot be found, go to the method list of the parent class and find the NSObject class until it is found.
  6. If you still haven't found it, you need to start into dynamic method parsing and message forwarding, as we will see later.

Why is it called "forwarding"? When an object does not have any operation that responds to a message, it "forwards" the message. The reason is that this technology is mainly for the object to let other objects process messages for them, thus "forwarding".

Message forwarding is a powerful technology that can greatly increase the expressiveness of Objective-C. What is message forwarding? In short, it allows unknown information to be trapped and reacted to. In other words, whenever an unknown message is sent, it will be sent to your code as a good package, at which point you can do whatever you want.

1.3 The Methodological Essence of OC

The method in OC is hidden by default by two parameters: self and _cmd. You may know that self is passed as an implicit parameter, which eventually becomes a definite parameter. The little-known implicit parameter _cmd, which holds the selector of the message being sent, is the second such implicit parameter. In short, self points to the object itself and _cmd points to the method itself. Two examples are given to illustrate:

  • Example 1: - (NSString *)name
    This method actually has two parameters: self and _cmd.

  • Example 2: - (void)setValue:(int)val
    This method actually has three parameters: self,_cmd and val.

At compile time, the OC function call syntax you write 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];
  • C
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

The objc_msgSend is responsible for sending messages.

2. Dynamic characteristics: method parsing and message forwarding

Without the implementation of the method, the program will hang up at runtime and throw unrecognized selector sent to... Exception. But before the exception is thrown, the runtime of Objective-C gives you three chances to save the program:

  • Method resolution
  • Fast forwarding
  • Normal forwarding

2.1 Dynamic Method Analysis: 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 a function and return YES, the runtime system will restart the process of sending a message. Or take foo for 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];
}

Here the first character v represents the function return type void, the second character @ represents the type id of self, and the third character: the type SEL representing _cmd. These symbols can be searched in Xcode developer documents for Type Encodings to see the corresponding meaning of symbols, more detailed official document portal Ad locum No longer listed here.

2.2 Fast forwarding: Fast Rorwarding

Before the message forwarding mechanism is implemented, the runtime system allows us to replace the recipient of the message for other objects. Through the -(id) forwarding Target ForSelector:(SEL) aSelector method. If this method returns nil or self, it enters the message forwarding mechanism (-(void) forward Invocation:(NSInvocation *) invocation), otherwise it will resend the message to the returned object.

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

2.3 Message Forwarding: Normal 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;
}

Forward Invocation: A distribution center that can't identify messages, forwards these unrecognized messages to different message objects, or forwards them to the same object, or translates them into other messages, or simply "eats" certain messages, so there is no response and no error. For example, in order to avoid flipping directly, we can give users a hint in this method when the message cannot be processed, which is also a friendly user experience.

Where does the parameter invocation come from? Before forward Invocation: message is sent, the runtime system sends the method Signature ForSelector: message to the object and retrieves the returned method signature to generate the NSInvocation object. So rewrite forward Invocation: while rewriting method SignatureForSelector: method, otherwise an exception will be thrown. When an object cannot respond to a message because it has no corresponding method to implement it, the runtime system will notify the object through forward Invocation: message. Each object inherits the forward Invocation: method, which allows us to forward messages to other objects.

3. Application Practice: Message Forwarding

3.1 Prevention and treatment of specific rush

Here is a section of code that runs out because there is no implementation method:

  • Test2ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test2ViewController";
    
    //Instantiate a button without implementing its method
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(50, 100, 200, 100);
    button.backgroundColor = [UIColor blueColor];
    [button setTitle:@"Message forwarding" forState:UIControlStateNormal];
    [button addTarget:self
               action:@selector(doSomething)
     forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

To solve this problem, a special classification can be created to deal with this problem:

  • NSObject+CrashLogHandle
#import "NSObject+CrashLogHandle.h"

@implementation NSObject (CrashLogHandle)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    //Method signature
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

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

@end

Because the method of the parent class is rewritten in category, the following warning will appear:

The solution is to ignore all warnings in the resource file in Xcode Build Phases, behind the corresponding file - w.

3.2 Apple System API Iteration Causes API Incompatible Run-out Processing

3.2.1 Traditional API Iteration Scheme for Compatible Systems

With the annual iteration of iOS system and hardware updates, some APIs with better performance or readability are likely to be discarded and replaced. At the same time, we also need to make version compatibility with the old API in the existing APP. Of course, there are many ways to make version compatibility. The following author will list several commonly used methods:

  • Judgment based on the response method
if ([object respondsToSelector: @selector(selectorName)]) {
    //using new API
} else {
    //using deprecated API
}
  • Judgment based on the existence of required classes in the current version of SDK
if (NSClassFromString(@"ClassName")) {    
    //using new API
}else {
    //using deprecated API
}
  • Judgment based on 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
}
A New API Iteration Scheme for 3.2.2 Compatible Systems

Requirement: Assume that there is a class written with a new API, as shown below, and one line of code that might run into a runaway situation on a low-level system (such as iOS 9):

  • 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];
}

One of them will issue a warning, Xcode also gives a recommended solution, if you click Fix, it will automatically add code to check the system version, as shown in the following figure:

Solution 1: Manually add version judgment logic

Previous adaptation processes can be judged by operating system versions

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

Scheme 2: Message forwarding

By adopting the latest API directly in iOS11 Base SDK and cooperating with Runtime's message forwarding mechanism, one line of code can be invoked differently in different versions of operating systems.

  • 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
  • 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

When we call the new API in iOS 10, we forward the original message to the UIViewController at the top of the current stack to invoke the lower version of the API because there is no specific implementation of the corresponding API.

For [self cm_topmostViewController]; the results after execution can be seen as follows:

The overall process of scheme 2:

  1. Returns a corresponding method signature for the message to be forwarded (which is later used for encoding the forwarding message object (NSInvocation *) an Invocation)

  2. Start message forwarding ((NSInvocation *) an Invocation encapsulates the call of the original message, including method name, method parameters, etc.)

  3. Because the API of forwarding calls is different from that of original calls, here we create a new NSI nvocation object viewController Invocation for message calls and configure the corresponding target and selector.

  4. Configuration parameters: Since each method actually defaults to two parameters: self and _cmd, we configure the other parameters from the third parameter.

  5. Message forwarding

3.2.3 Verification and comparison of new schemes

Note that when testing, select the iOS 10 system simulator to verify (if not Download Simulators). After installation, choose the following:

  • Unannotate and import UIScrollView+Forwarding class
  • Comment out the UIScrollView+Forwarding functional code

It will break down as shown in the following figure:

4. summary

4.1 Simulated Multiple Inheritance

Interview Digging: Does OC support multiple inheritance? Okay, you said you don't support multiple inheritance, so do you have a way to simulate multiple inheritance?

Forwarding is similar to inheritance in that it can be used to add some multi-inheritance effect to OC programming. An object forwards a message as if it were taking over or "inheriting" another object's playback. Message forwarding makes up for the fact that objc does not support multiple inheritance, and avoids the cumbersome complexity of a single class due to multiple inheritance.

Although forwarding implements inheritance, NSObject must be superficially rigorous, such as respondsToSelector: and isKindOfClass: these methods only consider inheritance systems, not forwarding chains.

4.2 Message Mechanism Summary

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

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

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

  3. If the resolve method returns NO, Runtime sends - forwarding Target ForSelector: allows you to forward this message to another object;

  4. If no new target object is returned, Runtime sends - Method Signature ForSelector: and - forward Invocation: messages. You can send - invokeWithTarget: message to manually forward the message or - doesNotRecognize Selector: throw an exception.

Posted by pyr on Tue, 25 Dec 2018 00:24:06 -0800