Cx330

「重学iOS」Objective-C中方法的传递过程

2020-12-19 · 12 min read
runtime Objective-C iOS

我们现在已经清楚方法的调用顺序了,实现从缓存中找没有的话再去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中,没有的话以此类推。流程如下:

01.png

当消息发送找到最后一个父类还没有找到对应的方法时,就会来到动态方法解析。动态解析,就是意味着开发者可以在这里动态的往rw_t中添加方法实现,这样的话系统再次遍历rw_t就会找到对应的方法进行调用了。

动态方法解析的流程示意图如下:

02.png

主要涉及到了两个方法:

+resolveInstanceMethod://添加对象方法  也就是-开头的方法
+resolveClassMethod://添加类方法  也就是+开头的方法

我们在实际项目中进行验证:

03.png

动态添加类方法也是如此,只不过是添加到元类对象中(此时run方法已经改成了个类方法):

04.png

而且我们也发现,动态添加方法的话其实无非就是找到方法实现,添加到类对象或元类对象中,至于这个方法实现是什么形式都没有关系,比如说我们再给对象方法添加方法实现时,这个实现方法可以是个类方法,同样给类方法动态添加方法实现时也可以是对象方法。也就是说系统根本没有区分类方法和对象方法,只要把imp添加到元类对象的rw_t中就是类方法,添加到类对象中就是对象方法。

05.png

当我们在消息发送和动态消息解析阶段都没有找到对应的imp的时候,系统回来到最后一个消息转发阶段。所谓消息转发,就是你这个消息处理不了后可以找其他人或者其他方法来代替,消息转发的流程示意图如下:

06.png

即分为两步,第一步是看能不能找其他人代你处理这方法,可以的话直接调用这个人的这个方法,这一步不行的话就来到第二部,这个方法没有的话有没有可以替代的方法,有的话就执行替代方法。我们通过代码来验证:

我们调用dog的run方法是,因为dog本身没有实现这个方法,所以不能处理。正好cat实现了这个方法,所以我们就将这个方法转发给cat处理:

07.png

我们发现,确实调用了小猫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的时候做任何逻辑。

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

08.png

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

09.png

关于源码阅读指南:

10.png