Level: ★★☆☆ Tag: “iOS” “message forwarding” “NULL” author: WYW Review: QiShare team


Preface:

In the development process, we may encounter null in the data returned by the server. When null value is obtained and the message is sent to null, it may appear that the unrecognized selector sent to instance, apply crash. In this case, it is not appropriate to judge the value every time. Previously, I found a magical file NullSafe on GitHub: github.com/nicklockwoo… . Dragging the file into the project, even if null, does not report an unrecognized selector sent to instance. Recently, the author analyzed the NullSafe file and made a Demo: QiSafeType. The author will unveil the mysterious veil of NullSafe by introducing the message forwarding process.

Demo (QiSafeType) Interpretation of message forwarding

  • The author will call by demonstratingQiMessageAn instance of theqiMessageunrealizedlengthMethod to demonstrate the message forwarding process.
  • The effect of QiSafeType message forwarding is as follows:

QiSafeType message forwarding effect:

  1. qiMessageThe whole process of message forwarding mainly involves three methods:
    • + (BOOL)resolveInstanceMethod:(SEL)sel
    • - (id)forwardingTargetForSelector:(SEL)aSelector
    • - (void)forwardInvocation:(NSInvocation *)anInvocation
  2. Among them in+ (BOOL)resolveInstanceMethod:(SEL)selWhen, there will be the corresponding method cache operation, this operation is the system to help us do.

QiSafeType Partial resolution of message forwarding

  1. First post a picture of the message forwarding, and the author will talk about the content around this picture.

  2. The following author analyzes the process of message forwarding in turn

The following uses qiMessage to call the Length method as an example to analyze the process of message forwarding.

  • (1) FirstqiMessageIn the calllengthMethod, will be the first dynamic method resolution, call+ (BOOL)resolveInstanceMethod:(SEL)selWe can dynamically add methods here, and if we successfully add methods here, the system will dynamically add themlengthMethod to cache whenqiMessageCall againlengthMethod will not be called+ (BOOL)resolveInstanceMethod:(SEL)sel. The successful dynamic add is called directlylengthMethods.
  • (2) If the dynamic method parsing part we do not do the operation, or the dynamic adding method fails, will be carried outFind backup receiversThe process of- (id)forwardingTargetForSelector:(SEL)aSelectorThis process is used to find a receiver that can respondUnknown method aSelector.
  • (3) If the value returned in the process of looking for the backup receiver is nil, the complete message forwarding process will be entered.

The complete message forwarding flow: First create the NSInvocation object with all the details of the as-yet-unprocessed message enclosed in it, including the selector, target, and parameters. When the NSInvocation is invoked, the message-Dispatch system takes care of itself and assigns the message to the target. (52 Effective Ways to write high quality iOS and OS X from Effective Objective-C 2.0)

  1. In combination withQiMessageCode for further analysis of the message forwarding process
  • (1) Look at the first partqiMessageIn the calllengthMethod, will be the first dynamic method resolution, call+ (BOOL)resolveInstanceMethod:(SEL)selIf we are here forqiMessageDynamic add method. You can process messages, too.

The relevant codes are as follows:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    printf("%s:%s \n", __func__ ,NSStringFromSelector(sel).UTF8String);
    
    if (sel == @selector(length)) {
        BOOL addSuc = class_addMethod([self class], sel, (IMP)(length), "q@:");
        if (addSuc) {
            return addSuc;
        }
    }
    return [super resolveInstanceMethod:sel];
}

Copy the code

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, Const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); The “q@:” passed in as the types parameter stands for:

"Q" : return value long long; @ ": the instance calling the method is of object type" : ": indicates the methodCopy the code
  • If you need something else, look at the picture below to make it more intuitive

  • (2)qiMessageIn the calllengthMethod, the dynamic method parsing part if the return value is NO, will look for the backup receiver, call- (id)forwardingTargetForSelector:(SEL)aSelectorIf we are here for return can be processedlengthThe recipient of. You can process messages, too.

The relevant codes are as follows:

static NSArray *respondClasses;

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    printf("%s:%s \n", __func__ ,NSStringFromSelector(aSelector).UTF8String);

	id forwardTarget = [super forwardingTargetForSelector:aSelector];
    if (forwardTarget) {
        return forwardTarget;
    }
    
    Class someClass = [self qiResponedClassForSelector:aSelector];
    if (someClass) {
        forwardTarget = [someClass new];
    }
    
    return forwardTarget;
}


- (Class)qiResponedClassForSelector:(SEL)selector {
    
    respondClasses = @[
                       [NSMutableArray class],
                       [NSMutableDictionary class],
                       [NSMutableString class],
                       [NSNumber class],
                       [NSDate class],
                       [NSData class]
                       ];
    for (Class someClass in respondClasses) {
        if ([someClass instancesRespondToSelector:selector]) {
            return someClass;
        }
    }
    return nil;
}


Copy the code

There is not a common API: + (BOOL) instancesRespondToSelector (SEL) aSelector; This API is used to return whether an instance of a Class corresponds to an aSelector.

  • (3)qiMessageIn the calllengthMethod, if the return value of the dynamic method parsing part is NO, and the return value of the search for backup receiver is nil, a complete message forwarding process will be carried out. call- (void)forwardInvocation:(NSInvocation *)anInvocationThere will be an interlude in the process,- (NSMethodSignature *)methodSignatureForSelector:(SEL)selectorOnly we are there- (NSMethodSignature *)methodSignatureForSelector:(SEL)selectorWhen the corresponding NSMethodSignature instance is returned, the complete message forwarding process is completed.

First chat next episode – (NSMethodSignature *) methodSignatureForSelector (SEL) the selector.

Excerpts from the document: This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.

The bold part is the part that applies to our current scenario.

This method is also used for message forwarding when the NSInvocation object must be created. If our object can handle methods that are not directly implemented, we should override this method and return a proper method signature.

  • The relevant code
- (void)forwardInvocation:(NSInvocation *)anInvocation { printf("%s:%s \n\n\n\n", __func__ ,NSStringFromSelector(anInvocation.selector).UTF8String); anInvocation.target = nil; [anInvocation invoke]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { NSMethodSignature *signature = [super methodSignatureForSelector:selector]; if (! signature) { Class responededClass = [self qiResponedClassForSelector:selector]; if (responededClass) { @try { signature = [responededClass instanceMethodSignatureForSelector:selector]; } @catch (NSException *exception) { }@finally { } } } return signature; } - (Class)qiResponedClassForSelector:(SEL)selector { respondClasses = @[ [NSMutableArray class], [NSMutableDictionary class], [NSMutableString class], [NSNumber class], [NSDate class], [NSData class] ]; for (Class someClass in respondClasses) { if ([someClass instancesRespondToSelector:selector]) { return someClass; } } return nil; }Copy the code

There is not a common API: + (NSMethodSignature *) instanceMethodSignatureForSelector (SEL) aSelector; This API returns an instance of the method signature containing the instance method identifier description via the Class and the given aSelector.

In addition, I found a funny point about the NSInvocation. Again, take the example of 'qiMessage' calling the 'length' method. - (void)forwardInvocation:(NSInvocation *)anInvocation the anInvocation message is as follows: <NSInvocation: 0x6000025b8140> return value: {Q} 0 target: {@} 0x60000322c360 selector: {:} length > return value indicates the returned value. Q indicates that the returned value type is long. > target refers to the receiver of the message, and "@" identifies the object type; > Selector refers to a method, ":" means a method, and the length after it is the name of the method.Copy the code

NSInvocation Types:

It is doubtful points

Careful readers may notice that the process is not the first time a message is forwarded

+[QiMessage resolveInstanceMethod:]:length 
-[QiMessage forwardingTargetForSelector:]:length 
-[QiMessage forwardInvocation:]:length 
Copy the code

but

+[QiMessage resolveInstanceMethod:]:length 
-[QiMessage forwardingTargetForSelector:]:length 
+[QiMessage resolveInstanceMethod:]:length 
+[QiMessage resolveInstanceMethod:]:_forwardStackInvocation: 
-[QiMessage forwardInvocation:]:length 
Copy the code

+[QiMessage resolveInstanceMethod:]:length +[QiMessage resolveInstanceMethod:]:_forwardStackInvocation: Nsobject. mm source code is as follows:

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}
Copy the code

I’m not sure why. Readers who know please comment.

QiSafeType message forwarding code

  • QiSafeType message forwarding code is in QiMessage

NSNull+QiNullSafe.m

NullSafe: github.com/nicklockwoo… Write a copy of NSNull+ Qinullsafe.m.

  • NSNull+ Qinullsafe. m Can avoid the following problems:
    NSNull *null = [NSNull null];
    [null performSelector:@selector(addObject:) withObject:@"QiShare"];
    [null performSelector:@selector(setValue:forKey:) withObject:@"QiShare"];
    [null performSelector:@selector(valueForKey:) withObject:@"QiShare"];
    [null performSelector:@selector(length) withObject:nil];
    [null performSelector:@selector(integerValue) withObject:nil];
    [null performSelector:@selector(timeIntervalSinceNow) withObject:nil];
    [null performSelector:@selector(bytes) withObject:nil];
Copy the code

How does NullSafe handle the NULL problem

NullSafe handles the NULL problem using the third part of the message forwarding process.

However, in the development process, if possible, we should deal with the message forwarding part as early as possible, for example, when dynamic method parsing, dynamically add methods (after all, the system can do method caching for us at this step). Or an object that responds to an unimplemented method while looking for a backup receive object.

Note: Do not use any of these scenarios when testing, and try to expose the problem when testing. And when using the exception log, it is best to report.

Reference study materials

  • NSObject.mm
  • Type Encodings
  • NullSafe
  • Effective Objective-C 2.0 52 Effective ways to write high quality iOS and OS X

Recommended articles:

IOS custom drag-and-drop control: QiDragView iOS custom card control: QiCardView iOS Wireshark Capture iOS Charles capture TCP IP address and UDP QiCardView