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
meaning
with 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: self
and _cmd
. As you probably know self
is 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, self
to the object itself, _cmd
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
andval
.
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:
- 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.
- 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.
- 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.
- If it is not found in the cache, find out whether there is a corresponding method in the method list of the class.
- 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.
- 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 v
represents the function return type void
, the second character @
represents the type of self , and the id
third 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)aSelector
method. 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 invocation
come 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:
-
Return a corresponding method signature for the message to be forwarded (the signature is later used to encode the forwarding message object (NSInvocation *) anInvocation)
-
Start message forwarding ((NSInvocation *)anInvocation encapsulates the invocation of the original message, including method names, method parameters, etc.)
-
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
-
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.
-
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:
-
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;
-
If not found, Runtime will send
+resolveInstanceMethod:
or+resolveClassMethod:
try to resolve this message; -
If the resolve method returns NO, the Runtime sends a message that
-forwardingTargetForSelector:
allows you to forward the message to another object; -
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.