消息的发送与转发还是利用到oc中的黑魔法runtime实现的
消息发送
什么称之为消息发送?在oc中方法的调用称之为向对象发送消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//B_ViewController.h
@interface B_ViewController : UIViewController
- (void)sendMessageTest;
@end
//B_ViewController.m
@implementation B_ViewController
- (void)sendMessageTest
{
NSLog(@"sendMessageTest");
}
@end
在A_ViewController中初始化并调用方法
B_ViewController *bClass = [B_ViewController new];
[bClass sendMessageTest];
上面是在A类中调用B类的实例方法简单模式
当我们将 B_ViewController.m中的实现方法屏蔽掉之后,会发现系统运行后报错了
这就是要研究的问题:
- [bClass sendMessageTest] 是如何调用的
- 没有实现方法报错的原因
[bClass sendMessageTest] 是如何实现的
1
2
3
4
5
[bClass sendMessageTest]
||
|| 编译后,实际是调用下面的objc_msgSend方法
||
((void (*)(id, SEL))(void *) objc_msgSend)(bClass, @selector(sendMessageTest));
本质上是 runtime 在运行时期间,根据传递的两个参数来寻找到对应的实例方法
runtime如何找到对应的实例方法
isa指针如何关联
先要弄明白 实例对象、类对象、meta类对象、根meta类对象 它们之间的联系,下图简单标明了

从上面是可以看出:它们之间是通过一个叫 isa 的指针来关联起来.
那么具体的关系可以概述为:
实例对象通过 isa 指针,找到类对象 Class;类对象同样通过 isa 指针,找到元类对象;元类对象也是通过 isa 指针,找到根元类对象;最后,根元类对象的 isa 指针,指向自己。可以发现 NSObject 是整个消息机制的核心,绝大数对象都继承自它。
isa指针的寻找过程
1、先看下系统内部的object 结构体
1
2
3
4
5
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
可以看出 只有一个指向Class的指针
2、接下里再来看下 Class 结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
从上面可以看出 有 objc_method_list这个结构体的存在,
3、我们来看下 objc_method_list结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
从上面可以看出 又存在另一个 objc_method的结构体
4、来看下objc_method结构体
1
2
3
4
5
6
7
8
9
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
method里面保存了三个参数
- 方法名称
- 方法类型
- 方法的IMP(IMP与SEL是键值对)
经过一层层的深入,大致就明白了 实例对象调用方法的逻辑
- 先被编译成 ((void (*)(id, SEL))(void *) objc_msgSend)(bClass, @selector(sendMessageTest));
- 沿着入参 bClass 的 isa 指针,找到 bClass 的类对象(Class)
- 接着在 bClass 的方法列表 methodLists 中,找到对应的 Method
- 最后找到 Method 中的 IMP 指针,从而得到具体实现方法SEL
runtime如何找到对应的类方法
其实类方法也一样,传入的不是实例对象,而是一个类
1
[B_ViewController sendMessageClassMethod];
那么我们首先通过类的isa指针,找到其meta类,在meta类中也存在methodList,步骤就和寻找实例方法一样
runtime寻找的效率
上面寻找实例方法、类方法都是是作了一个最简单的说明,都是在子类中就能寻找到,如果在子类中寻找不到,那么就会一级一级往父类寻找,同一个方法寻找对此又是如何实现的呢?
1
2
3
4
5
for (int i = 0; i < 100000; ++i)
{
BClass *bObject = [BClass new];
[bObject method];
}
上面的无限大循环就是一个很明显的例子,不可能说每次寻找都会一层一层去遍历,所以就引入了 Class Cache
类缓存
Class Cache 认为,当一个方法被调用,那么它之后被调用的可能性就越大。
回头看下上面的 Class 结构体,会发现中间就有一项
1
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
这个缓存是只存在于类中,对象中是不存在的
如何利用缓存进行遍历
查找方法时,会先从缓存中查找,找到直接返回 ;找不到,再去 Class 的方法列表中找。
子类没有就去父类寻找,找到之后也会同时将其添加到缓存

上面所说的都是成功寻找到了,还有可能找不到,这个怎么处理呢?
就是下面的消息转发
消息转发
_objc_msgForward
IMP类型,用于消息转发。当向一个对象发送一条消息,但它并没有实现的时候,objc_msgForward会尝试做消息转发(消息发送就是查找IMP,没找到时就用该方法替代IMP)
消息转发所做的几件事(后面详细介绍3个补救方法)
-
调用
resolveInstanceMethod:方法 (或resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。 -
调用
forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。 -
调用
methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。 -
调用
forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。 -
调用
doesNotRecognizeSelector:,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
没有实现方法报错的原因
这是针对上面的第二个问题作出回答,寻找不到方法并不会直接报错,系统有三次补救的机会,三次补救机会都没有找到方法实现才会crash
未实现的方法
1
2
// ViewController.m 中 (未实现 myLogAction 方法)
[self performSelector:@selector(myLogAction:) withObject:nil];
第一次补救
1
2
3
4
//针对实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//针对类方法
+ (BOOL)resolveClassMethod:(SEL)sel;
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ViewController.m 中
void myMethod(id self, SEL _cmd,NSString *nub) {
NSLog(@"%@",nub);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (sel == @selector(myLogAction:)) {
#pragma clang diagnostic pop
class_addMethod([self class],sel,(IMP)myMethod,"newMethodDesc");
return YES;
}else {
return [super resolveInstanceMethod:sel];
}
}
我们只需要在 resolveInstanceMethod: 方法中,利用 class_addMethod 方法,将未实现的 myLogAction: 绑定到 myMethod 上就能完成转发,最后返回 YES。
第二次补救
1
- (id)forwardingTargetForSelector:(SEL)aSelector;
这个方法要求返回一个 id。使用场景一般是将 A 类的某个方法,转发到 B 类的实现中去。
使用示例:
这里就是 VC中的方法,转发到 BVC类的实现中去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// BVC.m
@implementation BVC
- (void)myLogAction:(NSString *)str {
NSLog(@"%@",str);
}
@end
// ViewController.m 中
- (id)forwardingTargetForSelector:(SEL)aSelector {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (aSelector == @selector(myLogAction:)) {
#pragma clang diagnostic pop
return [BVC new];
}else{
return [super forwardingTargetForSelector:aSelector];
}
}
第三次补救
1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
第一个要求返回一个方法签名,第二个方法转发具体的实现。二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。
这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活,forwardingTargetForSelector: 只能固定的转发到一个对象;forwardInvocation: 可以让我们转发到多个对象中去。
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// BVC.m
@implementation BVC
- (void)myLogAction:(NSString *)str {
NSLog(@"%@",str);
}
@end
// CVC.m
@implementation CVC
- (void)myLogAction:(NSString *)str {
NSLog(@"%@",str);
}
@end
// ViewController.m 中
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (aSelector == @selector(myLogAction:)) {
#pragma clang diagnostic pop
return [NSMethodSignature signatureWithObjCTypes:"methodSignatureDesc"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
BVC *b = [BVC new];
CVC *c = [CVC new];
if ([b respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:b];
}
if ([c respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:c];
}
}
如果到了第三次机会,还没找到对应的实现,就会 crash:
unrecognized selector sent to instance 0x7f9f817072b0