我们现在已经清楚方法的调用顺序了,实现从缓存中找没有的话再去rw_t中找,那么再没有的话就去其父类中找,父类中查找也是如此,先去父类中的cache中查找,没有的话再去父类的rw_t中找,以此类推。如果查找到基类还没有呢?难道就直接报unrecognized selector sent to instance
这个经典错误吗?
其实不是,方法的传递主要涉及到三个部分,这也是我们平时用得最多以及面试中经常出现的问题:
我们都知道,当我们调用一个方法是,其实底层是将这个方法转换成了objc_msgSend
函数来进行调用,objc_msgSend
的执行流程可以分为3大阶段:
消息发送->动态方法解析->消息转发
****这个流程我们是可以从源码中得到确认,以下是源码:
1 /***********************************************************************
2 * _class_lookupMethodAndLoadCache.
3 * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
4 * This lookup avoids optimistic cache scan because the dispatcher
5 * already tried that.
6 **********************************************************************/
7 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
8 {
9 return lookUpImpOrForward(cls, sel, obj,
10 YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
11 }
12
13
14 /***********************************************************************
15 * lookUpImpOrForward.
16 * The standard IMP lookup.
17 * initialize==NO tries to avoid +initialize (but sometimes fails)
18 * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
19 * Most callers should use initialize==YES and cache==YES.
20 * inst is an instance of cls or a subclass thereof, or nil if none is known.
21 * If cls is an un-initialized metaclass then a non-nil inst is faster.
22 * May return _objc_msgForward_impcache. IMPs destined for external use
23 * must be converted to _objc_msgForward or _objc_msgForward_stret.
24 * If you don't want forwarding at all, use lookUpImpOrNil() instead.
25 **********************************************************************/
26 //这个函数是方法调用流程的函数 即消息发送->动态方法解析->消息转发
27 IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
28 bool initialize, bool cache, bool resolver)
29 {
30 IMP imp = nil;
31 bool triedResolver = NO;
32
33 runtimeLock.assertUnlocked();
34
35 // Optimistic cache lookup
36 if (cache) {
37 imp = cache_getImp(cls, sel);
38 if (imp) return imp;
39 }
40
41 // runtimeLock is held during isRealized and isInitialized checking
42 // to prevent races against concurrent realization.
43
44 // runtimeLock is held during method search to make
45 // method-lookup + cache-fill atomic with respect to method addition.
46 // Otherwise, a category could be added but ignored indefinitely because
47 // the cache was re-filled with the old value after the cache flush on
48 // behalf of the category.
49
50 runtimeLock.lock();
51 checkIsKnownClass(cls);
52
53 if (!cls->isRealized()) {
54 realizeClass(cls);
55 }
56
57 if (initialize && !cls->isInitialized()) {
58 runtimeLock.unlock();
59 _class_initialize (_class_getNonMetaClass(cls, inst));
60 runtimeLock.lock();
61 // If sel == initialize, _class_initialize will send +initialize and
62 // then the messenger will send +initialize again after this
63 // procedure finishes. Of course, if this is not being called
64 // from the messenger then it won't happen. 2778172
65 }
66
67
68 retry:
69 runtimeLock.assertLocked();
70
71 // Try this class's cache.
72 //先从当前类对象的方法缓存中查看有没有对应方法
73 imp = cache_getImp(cls, sel);
74 if (imp) goto done;
75
76 // Try this class's method lists.
77 //没有的话再从类对象的方法列表中寻找
78 {
79 Method meth = getMethodNoSuper_nolock(cls, sel);
80 if (meth) {
81 log_and_fill_cache(cls, meth->imp, sel, inst, cls);
82 imp = meth->imp;
83 goto done;
84 }
85 }
86
87 // Try superclass caches and method lists.
88 {
89 unsigned attempts = unreasonableClassCount();
90 //遍历所有父类 知道其父类为空
91 for (Class curClass = cls->superclass;
92 curClass != nil;
93 curClass = curClass->superclass)
94 {
95 // Halt if there is a cycle in the superclass chain.
96 if (--attempts == 0) {
97 _objc_fatal("Memory corruption in class list.");
98 }
99
100 // Superclass cache.
101 //先查找父类的方法缓存
102 imp = cache_getImp(curClass, sel);
103 if (imp) {
104 if (imp != (IMP)_objc_msgForward_impcache) {
105 // Found the method in a superclass. Cache it in this class.
106 log_and_fill_cache(cls, imp, sel, inst, curClass);
107 goto done;
108 }
109 else {
110 // Found a forward:: entry in a superclass.
111 // Stop searching, but don't cache yet; call method
112 // resolver for this class first.
113 break;
114 }
115 }
116
117 // Superclass method list.
118 //再查找父类的方法列表
119 Method meth = getMethodNoSuper_nolock(curClass, sel);
120 if (meth) {
121 log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
122 imp = meth->imp;
123 goto done;
124 }
125 }
126 }
127
128 // No implementation found. Try method resolver once.
129 //消息发送阶段没找到imp 尝试进行一次动态方法解析
130 if (resolver && !triedResolver) {
131 runtimeLock.unlock();
132 _class_resolveMethod(cls, sel, inst);
133 runtimeLock.lock();
134 // Don't cache the result; we don't hold the lock so it may have
135 // changed already. Re-do the search from scratch instead.
136 triedResolver = YES;
137 //跳转到retry入口 retry入口就在上面,也就是x消息发送过程即找缓存找rw_t
138 goto retry;
139 }
140
141 // No implementation found, and method resolver didn't help.
142 // Use forwarding.
143 //消息发送阶段没找到imp而且执行动态方法解析也没有帮助 那么就执行方法转发
144 imp = (IMP)_objc_msgForward_impcache;
145 cache_fill(cls, sel, imp, inst);
146
147 done:
148 runtimeLock.unlock();
149
150 return imp;
151 }
首先,消息发送,就是我们刚才提到的系统会先去cache_t中查找,有的话调用,没有的话去类对象的rw_t中查找,有的话调用并缓存到cache_t中,没有的话根据supperclass指针去父类中查找。父类查找也是如此,先去父类的cache_t中查找,有的话进行调用并添加到自己的cache_t中而不是父类的cache_t中,没有的话再去父类的rw_t中查找,有的话调用并缓存到自己的cache_t中,没有的话以此类推。流程如下:
当消息发送找到最后一个父类还没有找到对应的方法时,就会来到动态方法解析。动态解析,就是意味着开发者可以在这里动态的往rw_t中添加方法实现,这样的话系统再次遍历rw_t就会找到对应的方法进行调用了。
动态方法解析的流程示意图如下:
主要涉及到了两个方法:
+resolveInstanceMethod://添加对象方法 也就是-开头的方法
+resolveClassMethod://添加类方法 也就是+开头的方法
我们在实际项目中进行验证:
动态添加类方法也是如此,只不过是添加到元类对象中(此时run方法已经改成了个类方法):
而且我们也发现,动态添加方法的话其实无非就是找到方法实现,添加到类对象或元类对象中,至于这个方法实现是什么形式都没有关系,比如说我们再给对象方法添加方法实现时,这个实现方法可以是个类方法,同样给类方法动态添加方法实现时也可以是对象方法。也就是说系统根本没有区分类方法和对象方法,只要把imp添加到元类对象的rw_t中就是类方法,添加到类对象中就是对象方法。
当我们在消息发送和动态消息解析阶段都没有找到对应的imp的时候,系统回来到最后一个消息转发阶段。所谓消息转发,就是你这个消息处理不了后可以找其他人或者其他方法来代替,消息转发的流程示意图如下:
即分为两步,第一步是看能不能找其他人代你处理这方法,可以的话直接调用这个人的这个方法,这一步不行的话就来到第二部,这个方法没有的话有没有可以替代的方法,有的话就执行替代方法。我们通过代码来验证:
我们调用dog的run方法是,因为dog本身没有实现这个方法,所以不能处理。正好cat实现了这个方法,所以我们就将这个方法转发给cat处理:
我们发现,确实调用了小猫run方法,但是只转发方法执行者太局限了,要求接收方法对象必须实现了同样的方法才行,否则还是无法处理,所以实用性不强。这时候,我们可以通过methodSignatureForSelector来进行更大限度的转发。
需要注意的是要想来到methodSignatureForSelector
这一步需要**将*forwardingTargetForSelector
返回nil(即默认状态)*否则系统找到目标执行者后就不会再往下转发了。
**开发者可以在forwardInvocation:方法中自定义任何逻辑。
////为方法重新转发一个目标执行
//- (id)forwardingTargetForSelector:(SEL)aSelector{
// if (aSelector == @selector(run)) {
// //dog的run方法没有实现 所以我们将此方法转发到cat对象上去实现 也就是相当于将[dog run]转换成[cat run]
// return [[Cat alloc] init];
// }
// return [super forwardingTargetForSelector:aSelector];
//}
//方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
//注意:这里返回的是我们要转发的方法的签名 比如我们现在是转发run方法 那就是返回的就是run方法的签名
//1.可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。
//2.也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名
//这里使用self的话会进入死循环 所以不可以使用 如果其他方法中有同名方法可以将self换成其他类
// return [self methodSignatureForSelector:aSelector];
// return [NSMethodSignature instanceMethodSignatureForSelector:aSelector];
//3.直接输入字符串
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//当返回方法签名后 就会转发到这个方法 所以我们可以在这里做想要实现的功能 可操作空间很大
//这个anInvocation里面有转发方法的信息,比如方法调用者/SEL/types/参数等等信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//这样写不安全 可以导致cat被过早释放掉引发怀内存访问
// anInvocation.target = [[Cat alloc] init];
Cat *ca = [[Cat alloc] init];
//指定target
anInvocation.target = ca;
//对anInvocation做出修改后要执行invoke方法保存修改
[anInvocation invoke];
//或者干脆一行代码搞定
[anInvocation invokeWithTarget:[[Cat alloc] init]];
//上面这段代码相当于- (id)forwardingTargetForSelector:(SEL)aSelector{}中的操作
//当然 转发到这里的话可操作性更大 也可以什么都不写 相当于转发到的这个方法是个空方法 也不会报方法找不到的错误
//也可以在这里将报错信息提交给后台统计 比如说某个方法找不到提交给后台 方便线上错误收集
//...很多用处
}
当然我们也可以访问修改anInvocation的参数,比如现在run有个age参数,
// 参数顺序:receiver、selector、other arguments
int age;
//索引为2的参数已经放到了&age的内存中,我们可以通过age来访问
[anInvocation getArgument:&age atIndex:2];
NSLog(@"%d", age + 10);
我们发现,消息转发有两种情况,一种是forwardingTargetForSelector
,一种是methodSignatureForSelector+forwardInvocation:
其实,第一种也称快速转发,特点就是简单方便,缺点就是能做的事情有限,只能转发消息调用者;第二种也称标准转发,缺点就是写起来麻烦点,需要写方法签名等信息,但是好处就是可以很大成都的自定义方法的转发,可以在找不到方法imp的时候做任何逻辑。
当然,我们上面的例子都是通过对象方法来演示消息转发的,类方法同样存在消息转发,只不过对应的方法都是类方法,也就是-变+
所以,以上关于消息传递过程可以用下面这个流程图进一步总结:
关于源码阅读指南: