iOS message 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 trapped and react. In other words, whenever an unknown message is sent, it's sent to your code in a nice package, at which point you can do whatever you want.

Why is it called "forwarding"? A message is "forwarded" when an object does not have any action in response to that message. The reason is that this technique is primarily intended to allow objects to "forward" by letting other objects process messages for them.


1. Classes, objects, methods

Before we start using the message mechanism, we can agree on our terminology. For example, a lot of people don't know what a "method" vs a "message" is, but it's crucial to understanding how a messaging system works at a low level.

  • Method : A piece of actual code associated with a class, given a specific name. example:- (int)meaning { return 42; }
  • Message : The name and a set of parameters sent to the object. Example: Send to 0x12345678 object meaningwith no parameters.
  • selector : A special way of representing a message or method name, represented as type SEL. Selectors are essentially opaque strings that are managed so they can be compared using simple pointer equality, which increases speed. (Implementation may vary, but this is basically how they look externally.) For example: @selector(meaning).
  • Messaging : The process of receiving information and finding and executing the appropriate method.

1.1 OC methods and C functions

Objective-C methods are eventually generated as C functions with some extra parameters. Methods in Objective-C have two parameters hidden by default: selfand _cmd. As you probably know selfis passed as an implicit parameter, it ends up being an explicit parameter. The little-known implicit parameter _cmd(which holds the selector of the message being sent) is the second such implicit parameter. In short, selfto the object itself, _cmdto the method itself. Two examples are given to illustrate:

  • Example 1: - (NSString *)nameThis method actually has two parameters: selfand _cmd.

  • Example 2: - (void)setValue:(int)valThis method actually has three parameters: self, _cmdand val.

The syntax of an Objective-C function call you write is translated into a C function call at compile time 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);

1.2 C Expressions of Classes, Objects, and Methods

In Objective-C, classes, objects and methods are all C structures. From the objc/runtime.h and objc/objc.h header files, we can find their definitions:

  • objc_class
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
  • objc_object
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
  • objc_method
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • objc_method_list
struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}    

1.3 Message sending

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

  1. First check if this selector is to be ignored. For example, in Mac OS X development, if there is garbage collection, the retain and release functions will not be ignored.
  2. To detect whether the target of this selector is nil, OC allows us to execute any method on a nil object without crashing, because it will be ignored at runtime.
  3. If the above two steps are passed, start to find the implementation of this class IMP, first search 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 whether there is a corresponding method in the method list of the class.
  5. If it is not found in the method list of the class, it will search in the method list of the parent class until it finds the NSObject class.
  6. If you still can't find it, you will start to enter the dynamic method analysis, which will be said later.

2. Dynamic features: method resolution and message forwarding

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

  • Method resolution
  • Fast forwarding
  • Normal forwarding

2.1 Dynamic method resolution: Method Resolution

First, the Objective-C runtime will call + (BOOL)resolveInstanceMethod:or + (BOOL)resolveClassMethod:, giving you the opportunity to provide a function implementation. If you add the function and return YES, then the runtime system will restart the process of sending a message. Using foo as an example, you can do it like 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 vrepresents the function return type void, the second character @represents the type of self , and the idthird character :represents the type of _cmd SEL. These symbols can be searched for Type Encodings in the developer documentation in Xcode to see the corresponding meanings of the symbols. The more detailed official document portal is here , and will not be listed here.


2.2 Fast forwarding: Fast Rorwarding

Before the message forwarding mechanism is implemented, the runtime system allows us to replace the receiver of the message with another object. by - (id)forwardingTargetForSelector:(SEL)aSelectormethod. If this method returns nil or self, it will enter the message forwarding mechanism ( - (void)forwardInvocation:(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; } 

forwardInvocation:The method is a distribution center that does not recognize messages, forwards these unrecognized messages to different message objects, or forwards them to the same object, or translates messages into other messages, or simply "eats" some messages. message, so no response and no error are reported. For example, in order to avoid direct flashback, we can give the user a prompt in this method when the message cannot be processed, which is also a friendly user experience.

Where do the parameters invocationcome from? Before the forwardInvocation:message is sent, the runtime system will send a methodSignatureForSelector:message to the object, and get the returned method signature to generate the NSInvocation object. Therefore forwardInvocation:, when rewriting the method, you must also rewrite the methodSignatureForSelector:method, otherwise an exception will be thrown. When an object cannot respond to a message because it does not have a corresponding method implementation, the runtime system forwardInvocation:notifies the object with a message. Every object inherits forwardInvocation:methods, and we can forward messages to other objects.

3. Application combat: message forwarding

3.1 Specific crash prevention treatment

Here is a piece of code that crashes because the method is not implemented:

  • Test2ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test2ViewController";
    
    //实例化一个button,未实现其方法
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(50, 100, 200, 100);
    button.backgroundColor = [UIColor blueColor];
    [button setTitle:@"消息转发" forState:UIControlStateNormal]; [button addTarget:self action:@selector(doSomething) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } 

To solve this problem, a taxonomy can be created specifically to handle this kind of problem:

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

@implementation NSObject (CrashLogHandle)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    //方法签名
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}

@end

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


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


3.2 Crash handling caused by Apple system API iteration

3.2.1 Compatible with the traditional scheme of system API iteration

With the update and iteration of the iOS system and hardware every year, some APIs with better performance or higher readability may be discarded and replaced with the original APIs. At the same time, we also need to be compatible with the old API in the existing APP. Of course, there are many ways to achieve version compatibility. The author will list the commonly used ones below:

  • Judging by the method of responding
if ([object respondsToSelector: @selector(selectorName)]) {
    //using new API
} else {
    //using deprecated API
}
  • Judging based on whether the required class exists in the current version of the SDK
if (NSClassFromString(@"ClassName")) {    
    //using new API
}else {
    //using deprecated API
}
  • Judging by 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
}
3.2.2 A new scheme compatible with system API iteration

**Requirements:** Suppose there is a class written in the past, as shown below, in which there is a line of code that crashes because the system API is outdated:

  • 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 the lines will issue a warning, and Xcode also gives a recommended solution, if you click Fix it will automatically add code to check the system version, as shown below:


**Option 1:** Manually add version judgment logic

The previous adaptation process can be judged according to the operating system version

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

**Scenario 2: **Message Forwarding

The iOS11 Base SDK directly adopts the latest API and cooperates with the message forwarding mechanism of Runtime to realize a line of code and adopt different message calling methods under different versions of the operating system.

  • 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 iOS10, since there is no specific corresponding API implementation, we forward its original message to the UIViewController at the top of the current stack to call the lower version API.

Regarding [self cm_topmostViewController];, the results obtained after execution can be viewed as follows:


The overall process of scheme 2:

  1. Return a corresponding method signature for the message to be forwarded (the signature is later used to encode the forwarding message object (NSInvocation *) anInvocation)

  2. Start message forwarding ((NSInvocation *)anInvocation encapsulates the invocation of the original message, including method names, method parameters, etc.)

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

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

  5. message forwarding

3.2.3 Verification and comparison of new solutions

Note that when testing, select the simulator of the iOS10 system for verification (if not, you can download Simulators first). After installation, choose as follows:


  • Uncomment and import UIScrollView+Forwarding class

  • Comment out the function code of UIScrollView+Forwarding

It will crash as shown below:


4. Summary

4.1 Simulating Multiple Inheritance

Interview digging : Does OC support multiple inheritance? Well, you said that multiple inheritance is not supported, so do you have a way to simulate the multiple inheritance feature?

Forwarding is similar to inheritance, and can be used to add some multiple inheritance effects to OC programming. An object forwards a message out, as if he took over or "inherited" another object. Message forwarding makes up for the fact that objc does not support multiple inheritance, and also avoids the bloated and complicated single class caused by multiple inheritance.

Although forwarding can implement the inheritance function, NSObject must still be very strict on the surface. Methods like respondsToSelector:and isKindOfClass:will only consider the inheritance system, not the forwarding chain.

4.2 Summary of message mechanism

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

  1. Attempt to find the message in the object class's dispatch table. If found, jump to the corresponding function IMP to execute the implementation code;

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

  3. If the resolve method returns NO, the Runtime sends a message that -forwardingTargetForSelector:allows you to forward the message to another object;

  4. If no new target objects are returned, the Runtime sends -methodSignatureForSelector:and -forwardInvocation:messages . You can send a -invokeWithTarget:message to manually forward the message or send -doesNotRecognizeSelector:an exception.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325222316&siteId=291194637