OC Runtime 方法与消息传递
OC的方法调用我们已经很熟悉了,一段简单的代码,一个名为MyObject的类
#import <Foundation/Foundation.h>
@interface MyObject : NSObject
-(void)printSomeThing:(NSString *)age;
-(void)printSomeThing;
@end
#import "MyObject.h"
@implementation MyObject
-(void)printSomeThing:(NSString *)age {
NSLog(@"name = %@, age = %@",NSStringFromClass([self class]), age);
}
-(void)printSomeThing{
NSLog(@"come here");
}
@end
在一个ViewController中这样调用
#import "ViewController.h"
#import "MyObject.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
MyObject *object = [[MyObject alloc] init];
[object printSomeThing:@"kobe"];
[object printSomeThing];
}
通过 xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m 转成c++代码如下:
生成的ViewController.cpp,其中我们想要的核心代码
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
MyObject *object = ((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyObject"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)object, sel_registerName("printSomeThing:"), (NSString *)&__NSConstantStringImpl__var_folders_db_k2cngcsd0g91h3dbfbbgrjdr0000gn_T_ViewController_c46f7e_mi_0);
((void (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("printSomeThing"));
}
objc_msgSend
从上面的代码中我们发现,方法调用是通过 objc_msgSend 来实现的,编译器会把我们的代码转换成objc_msgSend
id objc_msgSend(id self, SEL op, ...);
self: 要接受消息的对象
op: 处理消息的方法的selector
…: 可变参数
返回值: 方法的返回值
当objc_msgSend 遇到方法调用时,编译器会选择调用下面方法中的一个
objc_msgSend 普通对象的消息发送
objc_msgSend_stret 当返回值是数据结构体时的息发送
objc_msgSendSuper 父类的消息发送
objc_msgSendSuper_stret 当返回值是数据结构体时父类的消息发送
在Apple的开放源码中有这样一句话相当重要:
These functions must be cast to an appropriate function pointer type
before being called。
这个方法调用前必须强制转成合适的函数指针类型。
在C语言中如果把函数指针强制转化成另一种类型,会出现行为未定义的错误,所以在上面的方法中先把objc_msgSend转成void *
然后再转成所需要的类型(void ()(id, SEL, NSString )),因为调用前必须强制转成合适的函数指针类型。另一个就转成了(void (**)(id, SEL))。
SEL
定义了一个不透明的类型来表示一个方法选择器
typedef struct objc_selector *SEL;
方法选择器用来表示runtime中的方法名字,方法选择器就是被注册在OC Runtime中一个C字符串,当一个类被加载时,由编译器生成的selector会自动注册到runtime中。
使用sel_registerName可以添加一个新的selector到runtime中,或者从runtime中获取一个已经存在的runtime。上面的代码中多次使用。
在使用selector时必须使用sel_registerName的返回值或者@selector()或者NSSelectorFromString(),不能直接把C字符串转成一个SEL。
在OC中如果方法相同,那么他们就会具有相同的selector,在runtime中他们就是同一个C字符串。在同一个类中不可能存在相同的方法,所以不用考虑如何区分。对于不同类中的相同的selector,用上一篇提到的isa来区分就ok了。
IMP
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
selector只是一个方法对应的名称字符串,而真正的实现是由IMP来实现的。IMP 就是为了表示函数的地址,本质就是一个函数指针,它指向了真正的函数实现过程。
那么SEL跟IMP是如何关联到一起的?
METHOD
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
Method把SEL跟IMP联系到了一起,这样通过SEL就可以找到相应的method然后对应到其相应的实现IMP.
id method_invoke(id receiver, Method m, ...); //调用method的实现
SEL method_getName(Method m); 获取method的名字 ,返回一个SEL
unsigned int method_getNumberOfArguments(Method m); 返回方法参数个数
IMP中的两个隐藏参数:
- self : 接收消息的对象
- _cmd : 此IMP对应的SEL
获取方法地址: methodForSelector
void (*setter)(id, SEL, BOOL); // 函数指针
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)]; //给setter函数指针赋值
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES); //函数调用
objc_msgSend动态绑定过程
- 根据SEL找到相应的IMP,由于不同的类可能具有相同的selector,所以在IMP的过程中要通过receiver的class来找到精确的IMP
- 调用IMP,把receiver,及参数传入IMP
- 返回其返回值
上图显示了消息的分发流程,当向一个对象发送消息时,首先根据对象的isa找到其对应的class,然后到class的cache中查找所要执行的selector,如果不存在继续到其方法列表中查找,如果没有,继续向上到其父类中查找,直到根类,如果在根类都没有找到相应的方法,那么就会报错,如果找到了就执行selector对应的IMP。
动态方法绑定
当我们向一个对象发送消息时,如果消息对应的selector不存在时就会出错。对于这样的问题抛出错误是一个相当明智的解决方法,这样就会杜绝很多bug。对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中我们可以动态的给selector绑定一个IMP, 这样selector就有了一个实现。
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"This is a dynimaic ");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(dynimaicSel)) {
class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "aaa@qq.com:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
dynimaicSel 没有实现, 通过class——addMethod为其添加了一个实现,相当于动态添加了一个Method。
当向一个对象发送一个未知的消息时,在系统报错前我们有三个时机来解决这问题,第一时机就是我们上一篇提到的使用resolveInstanceMethod:来动态加载一个实现,第二时机就是使用- (id)forwardingTargetForSelector:(SEL)aSelector把消息转发给另一个对象,第三个时机就是Message Forwarding。
-(id)forwardingTargetForSelector:(SEL)aSelector
用于把未知消息转发给另一个对象
当在类中实现此方法或者从父类继承来,方法的返回值non-nil就会作为消息的新的接收者。如果是nil,没有新的接收者时只需要return [super forwardingTargetForSelector:aSelector];
Forwarding
当对象收到一个未知的消息时,如果上面的步骤没有对消息进行都处理,在报错之前,runtime会向对象发送forwardInvocation:消息并且会携带一个NSInvocation对象作为唯一的参数。NSInvocation携带了原始的消息和参数。
在调用forwardInvocation方法之前,必须为调用的方法提供一个签名,我们需要重写methodSignatureForSelector方法,返回方法的签名。 如查返回nil,则不会继续往下执行,不会调用forwardInvocation方法。得到的错误仍然会是unrecognized selector
定义了两个类,一个Person, 一个Plane, 在Plane中定义了一个 -(void)fly;
在Person中重写了下面的函数来处理向person发送fly消息。
// 返回要调用方法的签名:instanceMethodSignatureForSelector
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (!sig) {
if ([Plane instanceMethodSignatureForSelector:aSelector]) {
sig = [Plane instanceMethodSignatureForSelector:aSelector];
}
}
return sig;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
if ([Plane instanceMethodSignatureForSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[Plane new]]; // 调用方法
} else {
[super forwardInvocation:anInvocation];
}
}