iOS 底层探索篇 —— 方法的转发流程

755 阅读8分钟

前言

方法查找失败

iOS 底层探索篇 —— 方法的查找流程这篇文章中,我们已经知道了方法的查找的流程了,如果方法没有查找到,在lookUpImpOrForward()函数里面还有一部分是留给查找失败的处理。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ...省略部分代码...

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver did not help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

  • // No implementation found. Try method resolver once.这一段注释告诉我们,imp没有,就还是方法解析。
  • // No implementation found, and method resolver did not help. // Use forwarding.这一段又是告诉我们,方法解析没有帮到我们,就利用forwarding转发。

接下来我们开始去分析下面两个流程。

动态方法解析

1. 函数入口

 if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }

_class_resolveMethod就是我们需要研究的函数。

2. 解析方法

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst); // 已经处理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 对象方法 决议
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

区分是元类还是类

  • 对象方法解析_class_resolveInstanceMethod
  • 类方法解析_class_resolveClassMethod。并且类方法解析以后,依旧没有找到imp,还是会走对象方法解析。

3. 对象与类方法解析

3.1 对象方法解析

/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver does not fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    ...省略部分代码...
}

3.2 类方法解析

/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver does not fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    ...省略部分代码...
}

两个函数其实是类似的。

  • if (! lookUpImpOrNil()这一个条件判断,主要是防止程序员写的类 不是继承自NSObject,就不需要开始动态方法解析流程了。
  • (typeof(msg))objc_msgSend,一个objc_msgSend消息发送SEL_resolveInstanceMethod或者SEL_resolveClassMethod
  • 在代码的注释段我们可以看到resolveInstanceMethod或者resolveClassMethod这么两个方法,意思就是告诉我们可以自己去实现父类NSObject的方法。

3.2 lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

动态方法解析之后,在开始查找流程,回到了iOS 底层探索篇 —— 方法的查找流程这篇文章的方法的慢速查找阶段。

4. 方法解析例子

4.1 对象方法解析例子

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(saySomething)) {
        NSLog(@"说话了");
        
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
    
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        
        const char *sayHType = method_getTypeEncoding(sayHMethod);
        
        return class_addMethod(self, sel, sayHIMP, sayHType);
    }
    
    return [super resolveInstanceMethod:sel];
}

将没有实现的方法saySomething,用另外一个已经实现了方法sayHello来替换。

4.2 类方法解析例子

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel == @selector(sayLove)) {
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
        
         Method method = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
        
        const char *types = method_getTypeEncoding(method);
        
        return class_addMethod(objc_getMetaClass("LGStudent"), sel, imp, types);
    }
    return [super resolveClassMethod:sel];
}

这里需要注意的位置就是,由于类方法在元类里面存着,我们添加方法的时候要在类的元类里面添加才能做到动态方法解析的成功。

4.3 留下问题

  1. 动态解析方法里面没有去处理,发现了这个问题,会进来了两次?

这个问题留到最后面解释。

  1. 在解析方法分析中,在类方法的动态方法解析中,还走下面的对象方法的解析的目的?
 _class_resolveClassMethod(cls, sel, inst); // 已经处理
 if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
         // 对象方法 决议
         _class_resolveInstanceMethod(cls, sel, inst);
    }
  • 我们通过iOS 底层探索篇 —— isa的初始化&指向分析这篇文章的分析,不管是类(针对对象方法),还是元类(针对类方法),他们最终的父类都会指向NSObject
  • 在方法的查找流程中,我们探索出来,实际上是通过selname来匹配的,在底层不分-、+方法的。
  • 目的就是可以让我们在NSObject中做相同的处理。

方法转发

如果动态方法解析没有处理,接下来会来到消息转发。

1. 转发来源分析

1.1 日志打印条件

iOS 底层探索篇 —— 方法的查找流程这篇文章中,我们知道方法查找到了之后就会进入缓存流程。

static void
log_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    _cache_fill (cls, meth, sel);
}
  • if (objcMsgLogEnabled)如果这个条件成立,那么就会进行日志打印。

1.2 日志打印入口

进行全部搜索objcMsgLogEnabled,可以发现

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
  • 条件赋值的函数。
  • 查看函数定义 OBJC_EXPORT void instrumentObjcMessageSends(BOOL flag)是一个可供外部使用的。

1.3 打印日志位置

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
 instrumentObjcMessageSends(true);
 [student saySomething];
 instrumentObjcMessageSends(false);
  • /tmp/msgSends这里是日志存放的位置。
  • instrumentObjcMessageSends方法需要定义一下extern表示外部使用。
  • commond + shift + G查看/tmp/msgSends

1.4 查看打印日志

+ LGStudent NSObject resolveInstanceMethod:
+ LGStudent NSObject resolveInstanceMethod:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject methodSignatureForSelector:
- LGStudent NSObject methodSignatureForSelector:
- LGStudent NSObject class
- LGStudent NSObject doesNotRecognizeSelector:
- LGStudent NSObject doesNotRecognizeSelector:
- LGStudent NSObject class

+ __NSCFString NSObject resolveInstanceMethod:
+ __NSCFString NSObject resolveInstanceMethod:

每个方法会打印两次的原因是,自己本身有一次,还有一次是super给干出来的。

  • 通过日志分析我们可以看到流程是resolveInstanceMethodforwardingTargetForSelectormethodSignatureForSelectordoesNotRecognizeSelector,每一步都没有实现就会报错了。

这里注意了,可以解决`4.3`遗留下来的问题,为什么会进来两次。看日志文件还有一次是__NSCFString系统级别处理了一次。

2. 快速转发

通过上面的日志我们可以看到快速转发流程forwardingTargetForSelector

2.1 NSObject.mm文件查看定义

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

可以让子类自己实现一下。

2.2 苹果官方定义

意思就是一个无法识别的消息可以让其他的对象来处理这个消息。

2.3 具体实现

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGStudent *student = [LGStudent alloc] ;
        [student saySomething];
    }
    return 0;
}

@interface LGTeacher : NSObject

@end

@implementation LGTeacher

- (void)saySomething{
    NSLog(@"%s",__func__);
}

@end

@implementation LGStudent

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}


@end

LGStudent这个类没有实现该saySomething的方法,但是LGTeacher实现了,就直接交给LGTeacher的对象处理。

3. 慢速转发

如果快速转发阶段也没有实现,就会进入到慢速转发阶段

3.1 方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(saySomething)) { 
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

这个位置就是对这个方法签名之后 丢出去,谁想要处理就去处理。

3.2 转发回调

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    SEL aSelector = [anInvocation selector];
    
    if ([[LGTeacher alloc] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[LGTeacher alloc]];
    else
        [super forwardInvocation:anInvocation];
}

如果可以让其他人处理就丢给另外的处理,否则系统就不处理这个方法了。

总结

1. 流程总结

消息转发的分析到这里就结束了,下面做一下总结。

  • 经过objc_msgSend方法快速查找和慢速查找不到结果之后就进入消息动态解析。
  • 动态解析过程_class_resolveMethod(),有处理就按照处理的来,没有处理,就进入消息快速转发阶段。
  • 消息快速转发阶段forwardingTargedForSelector(),有处理,就按照交给处理的对象来实现,有没有交给其他对象处理,进入慢速转发阶段。
  • 消息慢速转发阶段methodSignatureForSelector(),进行方法签名,把方法丢出去,forwardInvocation()来对消息处理。
  • 若以上中间步骤没有进行,则就进入到了doesNotRecognizeSelector报错。

附上流程图如下:

2. 观察转发流程控制台打印

控制台的打印顺序是按照resolveInstanceMethod -> forwardingTargetForSelector -> methodSignatureForSelector -> resolveInstanceMethod -> forwardInvocation

  • 倒数第二步把动态方法解析又调用了一次,这里实际上是上一步返回了一个方法签名之后,下一步就会查找这个签名,查找流程就会在走一次,这就是签名匹配的过程,再次调用_class_getInstanceMethod是系统级别做的。

学习之路,砥砺前行

不足之处可以在评论区指出