经过上面的学习我们知道,当调用方法[person test],就是给person对象发送test消息,然后通过isa->superclass-> superclass......寻找类对象,找到类对象之后,还会先通过@selector(test)&_mask生成索引,通过索引直接在cache里的散列表里面取方法,如果cache里面没有方法,再遍历methods数组......
OC的方法调用:消息机制,就是给方法调用者发送消息,其实都是转换为objc_msgSend函数的调用。
比如:
[person personTest];
转成C++代码就是:
objc_msgSend(person, sel_registerName("personTest"));
以前我们讲过sel_registerName("personTest")和@selector(personTest)返回的都是SEL,而且打印发现他们的地址的确也相同,所以上面的代码也可以写成:
objc_msgSend(person, @selector(personTest));
消息接收者:person
消息名称:personTest
意思就是给person对象发送personTest消息。
同理,类方法调用:
[MJPerson initialize];
转成C++代码:
(objc_getClass("MJPerson"), sel_registerName("initialize"));
也可以写成:
objc_msgSend([MJPerson class], @selector(initialize));
意思就是给MJPerson类对象发送initialize消息。
一. objc_msgSend的执行流程
objc_msgSend的执行流程可以分为3大阶段:
- 消息发送:就是根据isa、superclass寻找方法
- 动态方法解析:允许开发者动态创建新的方法
- 消息转发:转发给另外一个对象调用这个方法
objc_msgSend内部的这三个阶段经历完还找不到方法就报错:unrecognized selector sent to instance/class。
由于源码解析比较麻烦,我放到objc_msgSend源码解析这篇文章里面了,强烈建议先看一下,这样就很容易理解下面的流程,下面直接说结论。
一. 消息发送
二. 动态方法解析
下面通过动态添加方法来验证阶段二,动态方法解析。
1. 实例对象的动态方法解析
先创建MJPerson对象,只声明方法,不实现方法:
调用代码:
MJPerson *person = [[MJPerson alloc] init];
[person test];
[MJPerson test];
发现会报错:
unrecognized selector sent to instance 0x10074a610
unrecognized selector sent to class 0x100001118
下面动态添加实例方法,动态添加实例方法需要我们实现resolveInstanceMethod方法。
在MJPerson.m添加如下代码:
- (void)other
{
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 获取其他方法
Method method = class_getInstanceMethod(self, @selector(other));
// 动态添加test方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
打印:
-[MJPerson other]
可以发现方法添加成功。调用test,但是test没找到,会进入resolveInstanceMethod里面动态添加方法,方法添加完成会重新走消息发送流程,然后就找到了other,调用other,然后打印-[MJPerson other]。
补充:Method就是method_t
可能你不知道Method是什么,其实Method就是在iOS-底层-Runtime2里面讲过的method_t,method_t的结构体是这样的:
struct method_t {
SEL sel;
char *types;
IMP imp;
};
所以,上面的代码就能修改为:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 获取其他方法
struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
//Method几乎等价于以前讲的method_t,可打印验证
NSLog(@"%s, %s, %p",method->sel,method->types,method->imp);
// 动态添加test方法的实现
class_addMethod(self, sel, method->imp, method->types);
// 返回YES代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
执行代码,打印结果如下:
other, v16@0:8, 0x100000dd0
-[MJPerson other]
可以发现,函数名、函数编码(参数、返回值)、函数地址都打印出来了,other方法也调用了,说明动态方法添加成功,Method和method_t没啥区别。
2. 类对象的动态方法解析
那么如果调用的是类方法呢?
需要在resolveClassMethod方法里面,动态添加类方法。
MJPerson *person = [[MJPerson alloc] init];
//[person test];
[MJPerson test];
动态添加类方法:
//动态添加c语言函数的实现
void c_other(id self, SEL _cmd)//这也验证了OC的方法都有两个隐式参数(id self, SEL _cmd)
{
NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
//动态添加类方法
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 第一个参数是object_getClass(self)
//c语言的函数地址就是函数名
//object_getClass(self)获取元类对象
class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}
注意:动态添加实例方法传入到class_addMethod函数中的是当前类对象self,动态添加类方法传入到class_addMethod函数中的是元类对象object_getClass(self)。
上面代码运行后,打印:
c_other - MJPerson - test
可以发现添加成功,上面不但验证了类方法可以添加成功,而且验证了,还可以使用c语言函数作为他们的实现。
上面代码Demo地址:动态方法解析
三. 消息转发
关于消息转发的逻辑如上图,下面进行验证。
1. 实例对象的消息转发
① forwardingTargetForSelector返回对象
在MJPerson.m里面实现如下代码:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
// objc_msgSend([[MJCat alloc] init], aSelector)
return [[MJCat alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
执行:
MJPerson *person = [[MJPerson alloc] init];
[person test];
打印:
-[MJCat test]
可以发现,person对象没实现test方法,会调用MJCat的test方法。
那如果forwardingTargetForSelector返回值为空呢?
② methodSignatureForSelector返回方法签名
//方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
/*
NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
anInvocation.target 方法调用者
anInvocation.selector 方法名
[anInvocation getArgument:NULL atIndex:0] 获取参数
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// anInvocation.target = [[MJCat alloc] init];
// [anInvocation invoke];
//上面下面都可以了
[anInvocation invokeWithTarget:[[MJCat alloc] init]];
}
上面的代码,我们在返回方法签名之后,把anInvocation.target给修改为MJCat,所以最后还是会调用MJCat的test方法。
上面的代码,如果仅仅是想要调用MJCat的test方法,在forwardingTargetForSelector里面修改其实更简单。
在forwardInvocation做更多操作:
[person test]执行的代码其实就是forwardInvocation方法里面执行的代码,所以我们可以在forwardInvocation方法里面做任何操作,比如只打印,获取参数,获取返回值。
首先,MJPerson只声明不实现test方法,MJCat实现test方法,如下
@implementation MJCat
- (int)test:(int)age
{
return age * 2;
}
@end
在MJPerson.m里面实现如下代码:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test:)) {
//直接返回方法签名
//return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
//方法签名省略数字
return [NSMethodSignature signatureWithObjCTypes:"i@:I"];
//拿到MJCat的test方法签名来用
//return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//参数顺序:receiver(也就是self)、selector、other arguments
int age;
[anInvocation getArgument:&age atIndex:2];
NSLog(@"%d", age + 10);
//打印:15 + 10 = 25;
/*
anInvocation.target 是 [[MJCat alloc] init]
anInvocation.selector 是 test:
anInvocation的参数:15
*/
[anInvocation invokeWithTarget:[[MJCat alloc] init]];
int ret;
[anInvocation getReturnValue:&ret];
NSLog(@"%d", ret);
//打印:15 * 2 = 30;
}
关于返回方法签名、获取参数、获取返回值可看注释。
2. 类对象的消息转发
刚才讲的是实例对象的消息转发,如果是类对象需要实现+号开头的方法,如下:
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) return [MJCat class];
//+[MJCat test]
return [super forwardingTargetForSelector:aSelector];
}
返回MJCat类对象,当调用[MJPerson test],会打印:+[MJCat test],调用了MJCat的test方法。
上面的代码如果返回的是实例对象呢?如下:
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
// objc_msgSend([[MJCat alloc] init], @selector(test))
// [[[MJCat alloc] init] test]
if (aSelector == @selector(test)) return [[MJCat alloc] init];
//-[MJCat test]
return [super forwardingTargetForSelector:aSelector];
}
可以发现,返回MJCat实例对象,当调用[MJPerson test],会打印:-[MJCat test],调用了MJCat实例对象的对象方法,为什么会这样呢?
通过源码分析可知,forwardingTargetForSelector方法内部就是给返回的对象发送test消息,所以返回的是实例对象就是给实例对象发送消息,自然就是如下:
objc_msgSend([[MJCat alloc] init], @selector(test))
同样,如果上面方法返回为空,也会走以下代码:
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"1123");
}
打印:
1123
补充:@synthesize、@dynamic
首先在MJPerson.h写如下代码:
@property (assign, nonatomic) int age;
我们都知道,编译器会自动生成_age成员变量、setter和getter的声明、setter和getter的实现。
但是在很久以前,Xcode还没这么智能的时候,如果只写上面那句还不行,因为@property只负责生成settetr和getter方法的声明。还要在.m文件中使用@synthesize。
@synthesize age = _age, height = _height;
这时候编译器才会生成_age成员变量、setter和getter的实现。
如果使用@dynamic就是提醒编译器不要自动生成成员变量,不要自动生成setter和getter的实现,等到运行时再添加方法实现。
MJPerson.m代码如下:
@dynamic age;
void setAge(id self, SEL _cmd, int age)
{
NSLog(@"age is %d", age);
}
int age(id self, SEL _cmd)
{
return 120;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(setAge:)) {
class_addMethod(self, sel, (IMP)setAge, "v@:i");
return YES;
} else if (sel == @selector(age)) {
class_addMethod(self, sel, (IMP)age, "i@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
执行代码:
MJPerson *person = [[MJPerson alloc] init];
person.age = 20;
NSLog(@"%d", person.age);
打印:
age is 20
120
可以发现,使用@dynamic就没有生成_age成员变量、setter和getter的实现,这时候我们自己通过Runtime动态添加了setter和getter的实现才实现了如上打印。
总结:@synthesize自动生成_age成员变量、setter和getter的实现,@dynamic不自动生成_age成员变量、setter和getter的实现,正好是反过来的。
Demo地址:消息转发
面试题:
- 讲一下OC的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)。
objc_msgSend底层有3大阶段:消息发送(当前类、父类中查找)、动态方法解析、消息转发。
- 说一下消息转发流程
当消息发送和动态方法解析都没找到方法就会进入消息转发阶段
① 首先会调用+或-开头的forwardingTargetForSelector方法,如果这个方法返回值不为空,就给返回值发送SEL消息:objc_msgSend(返回值, SEL)。
② 如果这个方法的返回值为空,就会调用+或-开头的methodSignatureForSelector方法,如果这个方法返回值不为空,就会再调用+或-开头的forwardInvocation方法,我们可以在forwardInvocation里面方法做任何我们想做的事。
③ 如果这个方法的返回值为空,就会调用doesNotRecognizeSelector,报错unrecognized selector sent to instance/class。