Objective-C 方法调用就是一个对象消息传递的过程。在对象消息传递的过程中,涉及到了几个概念:
- 选择器:是一种文本字符串,用于指明调用的方法;
- 消息:
NSInvocation
封装了消息,包含消息的所有元素:目标target
、选择器SEL
、参数和返回值; - 方法签名
NSMethodSignature
:定义了方法参数类型和返回值类型。
1、选择器
在对象消息传递过程中,选择器是一种文本字符串,用于指明调用实例或类中的哪些方法。
选择器示例
logModel
sumA: b:
sumA: : //空选择器分段
在Objective-C中,消息的选择器直接与一个或者多个类方法/实例方法声明对应。
- (void)logModel;
- (int)sumA:(NSNumber *)a b:(int)b;
- (int)sumA:(int)a :(int)b;
编译代码时,编译器会创建数据结构和函数调用语句,使用它们以动态绑定将接收器和消息选择器的实现代码对应起来。在执行程序时,Runtime利用这些信息找到并调用适当的方法。
1.1、选择器类型 SEL
//SEL 的声明:是 objc_selector 类型的结构指针
typedef struct objc_selector *SEL:
SEL
是一种特殊的Objective-C数据类型,用于在编译源代码时替换选择器值的唯一标识符。相同选择器值的方法具有相同的SEL
。
创建SEL
变量:
-
@selector
指令:在编译时创建一个SEL
变量; -
NSSelectorFromString(NSString *)
函数:在程序运行时创建一个选择器变量。
2、对 Objective-C 消息的封装NSInvocation
NSInvocation 是以对象方法呈现封装的 Objective-C 消息。用于在对象之间、应用程序之间存储和转发消息,主要由NSTimer
对象和分布式对象系统来实现。
NSInvocation
对象包含Objective-C消息的必要元素:目标target
、选择器SEL
、参数和返回值。可以直接设置这些元素,并且在发送消息时自动设置返回值。
NSInvocation
对象可以重复地分派到不同的目标:它的参数可以根据不同的结果在调度之间进行修改;甚至它的选择器也可以更改为具有相同方法签名(参数和返回类型)的另一个选择器。这种灵活性使NSInvocation
可用于许多具有重复参数和变量的消息;而不是为每条消息重新输入略有不同的表达式,每次根据需要修改NSInvocation
对象,然后再将其分派给新目标。
NSInvocation
不支持使用可变数量的参数或联合参数调用方法。应该使用类方法+invocationWithMethodSignature:
创建NSInvocation
实例。
默认情况下,该类不会保留调用的参数。这些参数可能会释放,应该显式地保留这些参数,或者调用-retainArguments
方法来让调用对象保留它们本身。
注意:NSInvocation 符合NSCoding协议,但仅支持NSPortCoder编码。 NSInvocation不支持存档。
2.1、封装一个消息
/* 使用指定的方法签名 sig 构造消息
*/
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
//接收者的方法签名。
@property(readonly, retain) NSMethodSignature *methodSignature;
2.2、配置NSInvocation
2.2.1、配置接收器、选择器
//接收器的选择器,默认为 nil。
@property SEL selector;
//目标是发送消息的接收者。默认为 nil。
@property(assign) id target;
2.2.2、配置接收器的参数
/* 将 argumentLocation 的数据复制为index的参数
* @param 分配给接收器的参数的无类型缓冲区
* @param idx 索引;0 和 1 表示隐藏的参数 self 和 _cmd。通常使用 2 或者更大值
* @note 复制的字节数由参数大小决定
*/
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
/* 将存储在索引 idx 的参数复制到缓冲区指向的存储中 argumentLocation。
* @note 缓冲区的大小必须足够大以容纳参数值。
*/
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
当参数值是对象时,将指针传递给应从中复制对象的变量(或内存):
NSArray *anArray;
[invocation setArgument:&anArray atIndex:3];
[invocation getArgument:&anArray atIndex:3];
如果index
的值大于选择器的实际参数的数量,则此方法引发NSInvalidArgumentException
。
2.2.3、是否保留参数
为了提高效率,新创建的NSInvocation
实例不持有参数,也不持有其目标。 如果需要缓存,则应指示NSInvocation
对象保留其参数,否则在调用之前可能会释放参数。 NSTimer
对象总是指示其调用保留其参数,例如,因为在计时器触发之前通常会有一个延迟。
//如果接收者持有其参数,则为YES,否则为 NO。
@property(readonly) BOOL argumentsRetained;
/* 在调用此方法之前,argumentsRetained=NO
* 在调用此方法之后,argumentsRetained=YES
*
* 如果接收方尚未这样做,则保留接收方的目标和所有对象参数,并复制其所有C字符串参数和块
* 如果已设置returnvalue,则也会保留或复制该值。
*/
- (void)retainArguments;
2.2.4、接收者的返回值
/* 设置接收者的返回值。
* 通常在发送 -invoke 或 -invokeWithTarget:消息前需要设置此值。
*
* @param retLoc 一个无类型缓冲区,其内容被复制为接收方的返回值。
*/
- (void)setReturnValue:(void *)retLoc;
/* 接收器复制其返回值的无类型缓冲区。
* @param retLoc 应该足够大以容纳其值。
*/
- (void)getReturnValue:(void *)retLoc;
使用方法签名NSMethodSignature
的方法-methodReturnLength
确定缓冲区所需的大小:
NSUInteger length = [[myInvocation methodSignature] methodReturnLength];
buffer = (void *)malloc(length);
[invocation getReturnValue:buffer];
当返回值是对象时,将指针传递给应放置对象的变量(或内存):
id anObject;
NSArray *anArray;
[invocation1 getReturnValue:&anObject];
[invocation2 getReturnValue:&anArray];
如果从未调用过NSInvocation
对象,则此方法的结果是未定义的。
2.3、发送消息
/* 调用:将接收者的消息(带参数)发送到其目标并设置返回值。
* 注意:在调用此方法之前,必须设置接收器的目标,选择器和参数值。
*/
- (void)invoke;
/* 指定目标的调用:设置接收者的目标,将接收者的消息(带参数)发送到该目标,并设置返回值。
* 注意:在调用此方法之前,必须设置接收器的选择器和参数值。
*/
- (void)invokeWithTarget:(id)target;
3、方法签名NSMethodSignature
什么是方法签名?方法签名由方法返回类型的一个或多个字符组成,后面是隐式参数self
和_cmd
的字符串编码,接着可能是更多显式参数。
NSMethodSignature 以对象方式对方法签名的封装。使用methodReturnType
和methodReturnLength
属性确定字符串编码和返回类型的长度。使用-getArgumentTypeAtIndex:
方法和numberOfArguments
属性单独访问参数。在方法签名不匹配时,使用NSMethodSignature
转发消息。
/* 必须重写该方法,创建 NSMethodSignature 实例
* 消息转发通过该方法创建的 NSMethodSignature 创建 NSInvocation 对象
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *sign = [super methodSignatureForSelector:aSelector];
if (sign == nil && aSelector == NSSelectorFromString(@"unrecognized selector")){
if ([ModelHelper instancesRespondToSelector:aSelector]){
sign = [ModelHelper instanceMethodSignatureForSelector:aSelector];
}
}
return sign;
}
/* 将消息转发给其它对象处理
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if ([ModelHelper instancesRespondToSelector:anInvocation.selector]){
[anInvocation invokeWithTarget:self.helper];
}
}
类型编码
NSMethodSignature
对象是用代表返回字符串编码和方法参数类型的字符数组初始化的。使用@encode()
编译器指令获得特定类型的字符串编码。因为字符串编码是特定于实现的,所以不应该硬编码这些值。
例如,NSString
实例方法-containsString:
具有带以下参数的方法签名:
-
@encode(BOOL) (c)
:返回值类型 -
@encode(id) (@)
:接收者self
-
@encode(SEL) (:)
:选择器_cmd
-
@encode(NSString *) (@)
:第一个参数
3.1、创建方法签名
/* 返回指定Objective-C方法类型字符串的NSMethodSignature对象。
* @param types 包含方法参数的类型编码
*/
+ (NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
/* 获取指定位置参数的类型编码
* @param idx 参数索引;0 代表 self ,1 代表 _cmd;显式参数从索引2开始
* @return 该索引处参数的类型编码
* @note 如果索引超过参数数量,则引发异常NSInvalidArgumentException
*/
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx;
/* 接收器中的参数数量
* @note 至少有两个参数 self 和 _cmd
*/
@property(readonly) NSUInteger numberOfArguments;
/* 所有参数总共占用内存的字节数
* @note 内存因应用程序运行的硬件体系结构而异
*/
@property(readonly) NSUInteger frameLength;
/* 方法的返回类型
*/
@property(readonly) const char *methodReturnType;
/* 方法的返回值大小
*/
@property(readonly) NSUInteger methodReturnLength;
/* 确定同步状态
* @return 接收器异步调用,则为YES,否则为NO
* @note 如果方法是单向的,则远程消息的发送方不会阻止等待回复。
*/
- (BOOL)isOneway;