20 / 12 / 19

「重学 OC」内存管理06 - Objective-C中方法的传递过程

我们现在已经清楚方法的调用顺序了,实现从缓存中找没有的话再去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的时候做任何逻辑。

当然,我们上面的例子都是通过对象方法来演示消息转发的,类方法同样存在消息转发,只不过对应的方法都是类方法,也就是-变+

所以,以上关于消息传递过程可以用下面这个流程图进一步总结:

关于源码阅读指南: