经过上面的学习咱们知道,当调用办法[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。