我们都知道OC 是一门动态语言,所有的方法都是 通过runtime发送消息,所以Objective-C中调用方法其实就是向对象发送消息,比如:
PerSon *per = [[PerSon alloc]init];
[per testPerson];
- 这句代码的含义就是向对象obj发送testPerson的消息,编译器会调用底层的obj_msgSend( ),首先从缓存方法表中找到对应的IMP指针并执行。有时候在编写程序时,经常会遇到异常报错:
-
平时我们在开发中经常遇到到下图显示的问题:
为什么会出现这种问题,其实是调用一个不存在的方法就会遇到这个问题。
一个对象的方法像这样 [per testPerson] ,编译器转成消息发送objc_msgSend(per, testPerson),Runtime时执行的流程是这样的:
- 1.首先,通过per的isa指针找到它的 class(PerSon) ;
- 2.在 class (PerSon) 的 method list 找 testPerson方法 ;
- 3.如果class (PerSon) 中没到testPerson,通过super class指针 继续往它的 父类 中找 ;
- 4.一旦找到 testPerson 这个函数,就去执行它的实现IMP 。
- 5.如果没有找到,则进行方法解析,这个后面会提到。
但这种实现有个问题,执行效率很低。每次调用一个方法都要重复上面1-4的步骤。其实自然界存在一个法则就是28定律。一般有20%的方法会调用80%,所以苹果设计一个缓存机制,先来看看类的结构体。
首先我们来看看runtime的源码:
所以对一个对象发消息,底层调用一个 objc_msgSend(id theReceiver, SEL selectot,……)`方法系统执行的步骤为:
- 1、判断receiver(接受者)是否为nil,如果是nil的话则不往下执行,返回nil,这就是为什么在oc中一个nil发送消息不会奔溃的原因。
- 2、先从方法的缓存中查找 ,被调用过的方法会存在缓存里面,每个类都会有一个表来存被调用过的方法,以便下次更快的调用
- 3、如果缓存中没有找到则继续从本类的方法表(dispatch table)中查找方法寻找selector,如果找到则放到缓存中,继续执行该方法。
- 4、如果本类中没有找到selecto再从父类中查找方法,如此往复,直到达到基类。如果找不到则执行方法的动态解析。
继续用上面的例子,
下面我们通过实例来看一下在抛出异常之前也就是消息转发过程中都经过了哪些步骤:
第一步:类的动态方法解析
- 对象在收到无法解读的消息后,首先会调用
+(BOOL)resolveInstanceMethod:(SEL)sel或者+ (BOOL)resolveClassMethod:(SEL)sel, 询问是否有动态添加方法来进行处理,处理实例如下
// 第一步:类的动态方法解析:是为对象方法进行决议,
+ (BOOL)resolveInstanceMethod:(SEL)sel{
#if 1
NSString *selectorStr = NSStringFromSelector(sel);//方法名
NSLog(@"sel = %@",selectorStr);//sel = testPerson
/*
@param self 调用该方法的对象
@param sel 选择子
@param IMP 新添加的方法,是c语言实现的
@param 新添加的方法的类型,包含函数的返回值以及参数内容类型,eg:void xxx(NSString *name, int size),类型为:v@i
*/
if (sel == @selector(testPerson)) {
//向该类的实例对象中添加相应的方法实现 指定新的IMP
class_addMethod(self, sel, (IMP)newRun, "v@:");
/**
* i(类型为int)
* v(类型为void)
* @(类型为id)
* :(类型为SEL)
v代表返回值为void,@表示self,:表示_cmd
*/
return YES; //这里return NO;
}
return [super resolveInstanceMethod:sel];
#endif
}
//void newRun(id self,SEL sel,_cmd)
void newRun(id self,SEL sel) {
NSLog(@"对象方法的第一次转发-->> 本来给 PerSon 发送 testPerson 结果动态添加 newRun方法");
}
//是为类方法进行决议,
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(testSon)) {
Class clso = objc_getMetaClass(class_getName(self));
class_addMethod(clso, sel, (IMP)Run, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
void Run(id self,SEL sel) {
NSLog(@"类方法第一次转发====");
}
从打印结果看,成功实现了第一次转发
2019-05-29 19:22:08.169074+0800 BaseProject[1484:400363] sel = testPerson
2019-05-29 19:22:08.169174+0800 BaseProject[1484:400363] 对象方法的第一次转发-->> 本来给 PerSon 发送 testPerson 结果动态添加 newRun方法
消息转发第一步<resolveInstanceMethod>返回YES or NO?
不少初学runtime的同学都被这张图有点误导了,认为<resolveInstanceMethod>
返回NO才接着走后面的转发流程,而返回YES就停止转发了.
其实如果重写的resolveInstanceMethod什么也不做,只是返回YES也会接着走后面的转发流程。
这个返回值对于消息转发流程没有任何意义,从runtime的源码来看这个返回值只和debug的信息相关。
不管在resolveInstanceMethod方法中有没有动态添加方法,都会再去查找一次,所以这里不管是返回YES还是NO,对结果没有影响。从runtime的源码来看这个返回值只和debug的信息相关。
第二步:备用接受者对象 (消息的快速转发流程)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
return NO;
}
//第二步: 备用接受者对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
#if 1
NSString *selStr = NSStringFromSelector(aSelector);
if ([selStr isEqualToString:@"testPerson"]) {
Son *son = [[Son alloc] init];
if ([son respondsToSelector: aSelector]) {
return son;
}
}
return [super forwardingTargetForSelector:aSelector];
#endif
//return nil;
//如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。
}
//Son.m
- (void)testPerson{
NSLog(@"第二步 ------testSon");
}
可以看出第二步成功打印
2019-05-29 19:25:39.017002+0800 BaseProject[1489:401133] 第二步 ------testSon
如果我们不实现forwardingTargetForSelector或者此方法返回nil,系统就会调用方案三的两个方法methodSignatureForSelector和forwardInvocation
第三步:完整的消息转发 (消息的慢速转发流程)
//第三步
##### 用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。
//方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
#if 1
NSString *sel = NSStringFromSelector(aSelector);
// 判断要转发的SEL
if ([sel isEqualToString:@"testPerson"]) {
// 为转发的方法手动生成签名
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//v@:
}
return [super methodSignatureForSelector:aSelector];
#endif
//return nil; //消息无法处理
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
// 新建需要转发消息的对象
Dog *dog = [[Dog alloc] init];
if ([dog respondsToSelector:selector]) {
// 唤醒这个方法
[anInvocation invokeWithTarget:dog];
}else{
[self doesNotRecognizeSelector:selector];
}
}
//Dog.m
- (void)testPerson{
NSLog(@"第三步======dog =====eat");
}
2019-05-29 19:34:53.055100+0800 BaseProject[1510:403382] 第三步======dog =====eat
如果上面三个方法都没有实现则最后走doesNotRecognizeSelector方法
- (void)doesNotRecognizeSelector:(SEL)aSelector{
NSLog(@"任何方法都没有z走");
}
总结:
iOS中调用一个方法其实是对这个对象发送一个消息:底部会转成objc_msgSend函数,这个函数除了方法的参数之外还有两个隐藏的参数self和_cmd,接下来就会按照下面的流程去调用这个函数。
1.根据isa指针找到对象所属的类或者类所属的元类
2.先去类或者元类的cache列表中根据SEL(方法编号)去找这个方法。
//cache:因为Objective-C的消息转发需要查找dispatch table甚至可能需要遍历继承体系,所以缓存最近使用的方法。3没有找到,去method方法列表中找
4还是没有找到,就去父类中找
5 找到了,根据SEL(方法编号)找到对应的IMP(指向一个方法实现的指针),调用这个函数
6 没有则进入消息转发 (类的动态方法解析、备用接受者对象、完整的消息转发)。