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