一>形成由来:
Object-C是在C的基础上进行编写的一门动态程序语言(也称超C)。底层全部是由C语言实现的。所谓的类,转换到底层C的实现,即结构体等相关的代码。相比于静态编译的语言来说,OC在具体执行的时候才会查询到具体的方法指针,从而实现方法。
而也正是这种OC的动态语言特性造就了两大该语言自身的特色:
1.runtime
2.消息转发机制以及消息传递(msg_send)
本篇幅主要讨论第二种,消息传递与转发机制。
首先要明确的是,OC的方法是如何调用的。在C、C++等语言中,我们定义了一个函数,然后经由其他函数调用时,相当于效用指向所定义函数的指针地址。通过地址查询到了该函数,并随后经由此具体实现一个方法。而OC其实也是相同的。首先需要了解objc_class的结构。
我们所创建的每一个类,以及系统原生定义的工具类,都是遵循如下的class结构体所进行创建和存储的,而OC的Class的isa便指向一个类的结构体,其中包含众多该Class的详细信息:
struct objc_class {
struct objc_class super_class; /*父类*/
const char *name; /*类名字*/
long version; /*版本信息*/
long info; /*类信息*/
long instance_size; /*实例大小*/
struct objc_ivar_list *ivars; /*实例参数链表*/
struct objc_method_list **methodLists; /*方法链表*/
struct objc_cache *cache; /*方法缓存*/
struct objc_protocol_list *protocols; /*协议链表*/
};
由该结构可以看到,Class的isa所指向的结构体中,包含了该类的父类名称、该类的名称、版本信息、方法链表等等。而涉及到该Class的所有函数,是由methodLists方法链表进行保存。而OC中的方法存储是用一一对应的映射关系来进行管理的。如下图结构所示:
如上,方法名和具体的实现地址形成映射的关系。而这里,存储方法名指针selector的是SEL链表,对应存储方法实现地址指针的address的是IMP链表。
SEL:选择子。即方法名的指针,我们平时调用一个方法,都是通过输入方法名,转化为选择子(sel),然后通过该sel在所属类的方法链表中查询具体的方法的。如果查询到,则使用对应的函数地址(imp),查询到具体的实现函数并执行函数。如果查询不到,则涉及到消息的转发了。调用以下方法的时候,我们是能看出方法在调用时的状态的:
[self performSelector:<#(SEL)#>];
@selector(<#selector#>)
了解完了具体的Object-C的方法调用机制以后,我们就能理解消息转发机制能够形成的基础了。之所以留给消息转发实现的空间,就在于函数的具体执行,是在方法调用才从selector中寻找目标SEL,然后找到对应的IMP中的address来执行的。是动态编译的过程。因为,在Class本身查询SEL出现问题时,留出了一定的空间,供人操作改选择子(SEL)在本类动态添加或者其他对象中查询SEL对应的IMP实现方法,从而实现方法,防止报错。
完整的消息转发机制的过程:
从图上也可以看得出,消息转发总共分为三个阶段,层层递进。其中,到了第三个阶段,便称之为启动了完整的消息转发机制。当然,我们尽希望于在尽可能靠前的方法中,完成函数的处理,从而减少一定的性能消耗。
首先第一步:类内动态方法解析阶段
void testFunction(id self,SEL _cmd){
printf("通过动态解析以及runtime添加的方法,并得到执行");
}
//类对象的动态解析函数
+(BOOL)resolveClassMethod:(SEL)sel
{
//获取方法名称
NSString * selNameStr = [NSString stringWithFormat:@"%@",NSStringFromSelector(sel)];
//掩饰一个案例:当我们要识别的是一个testFunction方法,并动态添加
if ([selNameStr isEqualToString:@"testFunction"])
{
//为该类动态添加一个方法testFunction处理消息,并执行该方法
class_addMethod([self class], sel, (IMP)testFunction, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
//实例对象的动态解析函数
+(BOOL)resolveInstanceMethod:(SEL)sel
{
//获取方法名称
NSString * selNameStr = [NSString stringWithFormat:@"%@",NSStringFromSelector(sel)];
//掩饰一个案例:当我们要识别的是一个testFunction方法,并动态添加
if ([selNameStr isEqualToString:@"testFunction"])
{
//为该类动态添加一个方法testFunction处理消息,并执行该方法
class_addMethod([self class], sel, (IMP)testFunction, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
当第一步完成了SEL的处理,则方法得到执行,并结束。如若没有处理SEL,则进入第二步,启用备援接受者来处理该SEL。
第二步:启动备援接受者
第二步和第一步的区别在于,备援接受者方法在处理时可以向其他对象发送该SEL,并通过其他对象执行本对象不可执行的方法。这有别于动态方法解析,给自身动态添加未实现方法。当返回 非self\非nil 时,消息被转给新对象执行。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"unknow SEL's name is:%@",NSStringFromSelector(aSelector));
Class unknowClass = NSClassFromString(@"TargetClass");
NSObject * unknowOC = unknowClass.new;
if (aSelector == NSSelectorFromString(@"TargetClassFunction")) {
NSLog(@"找到了备援接受者");
return unknowOC;
}
return [self forwardingTargetForSelector:aSelector];
}
第二步,实质上是使用其他内部对象,来处理本类对象的未知SEL。你也可以利用该特性,营造出多重继承的效果。当第二步返回self、nil时,证明未被查到的SEL还没有找到合适的方法IMP。这是则走到第三步,启动完整的消息转发机制。
第三步:启动完整的消息转发机制
走到这一步时,系统会将那个未被处理的SEL封装为NSInvocation对象。里边详细封装了该SEL的所有信息。
需要注意的是methodSignatureForSelector,和forwardInvocation是配套使用的。如果你不实现方法签名,那么你是调用不到forwardInvocation函数的。这里就和NSInvocation对象本身创建就需要进行方法签名有关。以下是它的结构
/* NSInvocation.h
Copyright (c) 1994-2016, Apple Inc. All rights reserved.
*/
#import <Foundation/NSObject.h>
#include <stdbool.h>
@class NSMethodSignature;
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
@interface NSInvocation : NSObject {
@private
void *_frame;
void *_retdata;
id _signature;
id _container;
uint8_t _retainedArgs;
uint8_t _reserved[15];
}
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
@property (readonly, retain) NSMethodSignature *methodSignature;
- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;
@property (nullable, assign) id target;
@property SEL selector;
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)invoke;
- (void)invokeWithTarget:(id)target;
@end
//方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSString *sel = NSStringFromSelector(aSelector);
if ([sel rangeOfString:@"set"].location == 0)
{
//动态造一个 setter函数
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
} else {
//动态造一个 getter函数
return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
}
//完整消息转发
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
if ([company respondsToSelector:selector])
{
[anInvocation invokeWithTarget:company];
}
第三步,相比较于第二步,有一个显著的特点就是:你可以将该方法封装的NSInvocation对象,同时交给多个不同的对象去尝试实现。
疑问&思考:
.Q:转发机制的三个方法区别在于什么?为什么要层层递进,而不能使用一个万事大吉?
A:首先,方法1是在本类中利用runtime动态去做方法添加来处理;方法2通过转发给另外一个对象来进行处理;方法3是通过将这个未处理的方法的所有信息全部封装,创建NSInvocation对象,再开启完整转发机制。方法3相比于方法2,它可以给多个对象做转发,来处理该对象。
以上是三者的区别。但是,最大的问题我认为还是在于,层层递进之下,性能的消耗也是在不断攀升的。所以,在不考虑具体特定需求的情况下,我们还是遵照性能最优来处理。当然,如果有特定的需求,如多重继承等等,则根据实际情况,在做判断。
未完成部分:
1.NSInvocation的使用
2.消息转发的平时使用案例