There are some methods for passing functions in various languages: function pointers can be used in C language, and function references, imitation functions, and Lambda are available in C ++, and Selector is also available in objective-C) and block.
However, most of the APIs in ios sdk are selector, so this article focuses on selector.
Objective-C is different from other object-oriented languages I have come across. It emphasizes message transmission rather than method calls. Therefore, you can pass any message to an object without the need to process the message during compilation.
Obviously, since the method address cannot be determined during the compilation period, you need to locate the method during the runtime. The objective-C Runtime calls the method through the "id objc_msgsend (ID thereceiver, Sel theselector,...)" function. Thereceiver is the call object, theselector is the message name, and ellipsis is an indefinite parameter in C language.
The message name here is Sel type, which is defined as struct objc_selector *. However, the document does not reveal what the objc_selector is, but provides the @ selector command to generate:
SEL selector = @selector(message);
@ Selector is calculated during the compilation period, so it is not a function call. Further tests show that Mac OS X 10.6 and IoS are both C-style strings (char *):
NSLog (@"%s", (char *)selector);
You will find that the message name is "message.
Below is a test class:
@interface Test : NSObject@end@implementation Test- (NSString *)intToString:(NSInteger)number { return [NSString stringWithFormat:@"%d", number];}- (NSString *)doubleToString:(double *)number { return [NSString stringWithFormat:@"%f", *number];}- (NSString *)pointToString:(CGPoint)point { return [NSString stringWithFormat:@"{%f, %f}", point.x, point.y];}- (NSString *)intsToString:(NSInteger)number1 second:(NSInteger)number2 third:(NSInteger)number3 { return [NSString stringWithFormat:@"%d, %d, %d", number1, number2, number3];}- (NSString *)doublesToString:(double)number1 second:(double)number2 third:(double)number3 { return [NSString stringWithFormat:@"%f, %f, %f", number1, number2, number3];}- (NSString *)combineString:(NSString *)string1 withSecond:string2 withThird:string3 { return [NSString stringWithFormat:@"%@, %@, %@", string1, string2, string3];}@end
Then test objc_msgsend:
# Import <objc/message. h> // to use objc_msgsend, the header file test * test = [[test alloc] init]; cgpoint point = {123,456}; nslog (@ "% @", objc_msgsend (test, @ selector (pointtostring :), point); [test release];
The result is "{123.000000, 456.000000 }". In addition, like previously guessed, the following call is also possible:
NSLog(@"%@", objc_msgSend(test, (SEL)"pointToString:", point));
As you can see, you should find that this implementation method can only determine the message Name and number of parameters, and the parameter type and return type are erased. Therefore, the compiler can only warn you of incorrect parameter types during compilation, but cannot prevent you from passing parameters with incorrect types.
Next, let's take a look at some methods provided by the nsobject protocol for passing messages:
- -(ID) performselector :( SEL) aselector
- -(ID) performselector :( SEL) aselector withobject :( ID) anobject
- -(ID) specify mselector :( SEL) aselector withobject :( ID) anobject withobject :( ID) anotherobject
Didn't you think it was speechless? Why must the parameter be an object? Why does one support up to two parameters?
Fortunately, the selector itself does not care about the parameter type, so it is okay to pass a thing that is not an object:
NSLog(@"%@", [test performSelector:@selector(intToString:) withObject:(id)123]);
However, double and struct cannot be passed in this way, because they occupy different bytes and pointers. If you have to use javasmselector, you can only change the parameter type to pointer:
- (NSString *)doubleToString:(double *)number { return [NSString stringWithFormat:@"%f", *number];}double number = 123.456;NSLog(@"%@", [test performSelector:@selector(doubleToString:) withObject:(id)(&number)]);
The parameter type is done, but it takes time to support multiple parameters. Ideally, we can implement these two methods:
@interface NSObject (extend)- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;- (id)performSelector:(SEL)aSelector withParameters:(void *)firstParameter, ...;@end
First look at the former, nsarray requires that all elements must be objects and cannot be nil, so the applicability is still limited. But don't underestimate it, because you will find that you cannot use objc_msgsend to implement it, because you cannot predict the number of parameters when writing code.
Now it's nsinvocation:
@implementation NSObject (extend)- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects { NSMethodSignature *signature = [self methodSignatureForSelector:aSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self]; [invocation setSelector:aSelector]; NSUInteger i = 1; for (id object in objects) { [invocation setArgument:&object atIndex:++i]; } [invocation invoke]; if ([signature methodReturnLength]) { id data; [invocation getReturnValue:&data]; return data; } return nil;}@endNSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withObjects:[NSArray arrayWithObjects:@"1", @"2", @"3", nil]]);
Note the following three points:
- Because there are two implicit parameters, self (call object) and _ cmd (selector), the index should start from 2 when setting parameters.
- Because the parameter is an object, you must pass the pointer, that is, & object.
- If methodreturnlength is 0, it indicates that the return type is void. Therefore, you do not need to obtain the return value. When the returned value is an object, we do not need to create a buffer. However, if it is a C-style string, array, and other types, you need to manually malloc and release the memory.
Then we can implement the following 2nd methods:
- (id)performSelector:(SEL)aSelector withParameters:(void *)firstParameter, ... { NSMethodSignature *signature = [self methodSignatureForSelector:aSelector]; NSUInteger length = [signature numberOfArguments]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self]; [invocation setSelector:aSelector]; [invocation setArgument:&firstParameter atIndex:2]; va_list arg_ptr; va_start(arg_ptr, firstParameter); for (NSUInteger i = 3; i < length; ++i) { void *parameter = va_arg(arg_ptr, void *); [invocation setArgument:¶meter atIndex:i]; } va_end(arg_ptr); [invocation invoke]; if ([signature methodReturnLength]) { id data; [invocation getReturnValue:&data]; return data; } return nil;}NSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withParameters:@"1", @"2", @"3"]);NSInteger number1 = 1, number2 = 2, number3 = 3;NSLog(@"%@", [test performSelector:@selector(intsToString:second:third:) withParameters:number1, number2, number3]);
It is similar to the previous implementation, but because the parameter length is unknown, [Signature numberofarguments] is used. Of course, you can also convert sel into a string (nsstringfromselector () can be used), and then find the number.
Va_start, va_arg, and va_end are used to process variable parameters. If you are familiar with the C language, you can understand them.
However, because you do not know the parameter type, you can only set it to void *. This program also reported a warning that void * is incompatible with the nsinteger type. If you change the parameter to double, an error is reported. Unfortunately, I do not know how to determine whether a void * Pointer Points to the C data type or to an objective-C object, so it is best to encapsulate it into an objective-C object. If you only need to be compatible with the C type, you can remove the setargument parameter and directly pass the pointer in:
NSInteger number1 = 1, number2 = 2, number3 = 3;NSLog(@"%@", [test performSelector:@selector(intsToString:second:third:) withParameters:&number1, &number2, &number3]);double number4 = 1.0, number5 = 2.0, number6 = 3.0;NSLog(@"%@", [test performSelector:@selector(doublesToString:second:third:) withParameters:&number4, &number5, &number6]);[test release];
Multiple parameters can also be supported in this method for the callback mselector: withobject: afterdelay: method added to the nsobject class.
Next we will talk about the omitted _ cmd, which can also be used for recursive calls. The following uses the Fibonacci series as an example:
- (NSInteger)fibonacci:(NSInteger)n { if (n > 2) { return [self fibonacci:n - 1] + [self fibonacci:n - 2]; } return n > 0 ? 1 : 0;}
With _ cmd, the implementation becomes like this:
return (NSInteger)[self performSelector:_cmd withObject:(id)(n - 1)] + (NSInteger)[self performSelector:_cmd withObject:(id)(n - 2)];
Or directly use objc_msgsend:
return (NSInteger)objc_msgSend(self, _cmd, n - 1) + (NSInteger)objc_msgSend(self, _cmd, n - 2);
However, it is very difficult to use objc_msgsend every time. Is there a way to call the method directly? The answer is yes, So imp is needed. IMP is defined as "ID (* IMP) (ID, Sel ,...)", That is, a function pointer pointing to a method.
Nsobject provides methodforselector: method to obtain imp, so you only need to make a slight modification:
- (NSInteger)fibonacci:(NSInteger)n { static IMP func; if (!func) { func = [self methodForSelector:_cmd]; } if (n > 2) { return (NSInteger)func(self, _cmd, n - 1) + (NSInteger)func(self, _cmd, n - 2); } return n > 0 ? 1 : 0;}
Now the running time is 1/4 less than just now.
By the way, we will show the powerful dynamics of objective-C and add a sum: And: Method to the test class:
NSInteger sum(id self, SEL _cmd, NSInteger number1, NSInteger number2) { return number1 + number2;}class_addMethod([Test class], @selector(sum:and:), (IMP)sum, "[email protected]:ii");NSLog(@"%d", [test sum:1 and:2]);
Message transmission mechanism of ZZZ objective-C