OC源码分析之方法的解析与转发原理

2,875 阅读13分钟

前言

想要成为一名iOS开发高手,免不了阅读源码。以下是笔者在OC源码探索中梳理的一个小系列——类与对象篇,欢迎大家阅读指正,同时也希望对大家有所帮助。

  1. OC源码分析之对象的创建
  2. OC源码分析之isa
  3. OC源码分析之类的结构解读
  4. OC源码分析之方法的缓存原理
  5. OC源码分析之方法的查找原理
  6. OC源码分析之方法的解析与转发原理

OC中方法的调用是通过objc_msgSend(或objc_msgSendSuper,或objc_msgSend_stret,或objc_msgSendSuper_stret)函数,向调用者发送名为SEL的消息,找到具体的函数地址IMP,进而执行该函数。如果找不到IMP,会进行方法的解析,这相当于提供一次容错处理;方法解析之后,如果依然找不到IMP,还有最后一次机会,那就是消息的转发。

方法的查找流程尽在 OC源码分析之方法的查找原理 一文中,文接此文,本文将深入剖析方法的解析与转发。

下面进入正题。

需要注意的是,笔者用的源码是 objc4-756.2

1 方法的解析

方法的解析,即method resolver(又名消息的解析,也叫方法决议),其建立在方法的查找的失败结果上,入口源码如下:

    // 在【类...根类】的【缓存+方法列表】中都没找到IMP,进行方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

它主要是调用了resolveMethod函数。resolveMethod函数处理完毕之后,还要重新执行一次retry(再走一遍方法的查找流程)。其中,triedResolver这个变量使得消息的解析只进行一次。

1.1 resolveMethod

且看resolveMethod函数源码:

static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}

这里有两个分支,主要是对cls做个是否元类的判断:

  • 不是元类,意味着调用的是实例方法,那么执行resolveInstanceMethod函数
  • 是元类,说明调用的是类方法,执行resolveClassMethod函数,之后如果依然没找到IMP,则再去执行resolveInstanceMethod函数;

先看实例方法的情况

1.2 实例方法解析

resolveInstanceMethod源码如下:

static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // 如果你没有实现类方法 +(BOOL)resolveInstanceMethod:(SEL)sel
        // NSObject也有实现,所以一般不会走这里
        // 注意这里传入的第一个参数是:cls->ISA(),也就是元类
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 调用类方法: +(BOOL)resolveInstanceMethod:(SEL)sel
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // 再找一次imp(这次是sel,而不是resolveInstanceMethod)
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

resolveInstanceMethod函数先后调用了两次lookUpImpOrNil

  • 第一次的调用是判断类(包括其父类,直至根类)是否实现了+(BOOL)resolveInstanceMethod:(SEL)sel类方法
    • SEL_resolveInstanceMethod相当于@selector(resolveInstanceMethod:)NSObject类中有实现这个类方法(返回的是NO,会影响是否打印),所以一般会接着往下走。
  • 第二次的调用的目的是检测是否有sel对应的IMP。假如你在+(BOOL)resolveInstanceMethod:(SEL)sel中添加了sel的函数地址IMP,此时再次去查找这个IMP就能找到。

注意到这两次调用中,resolver都是NO,因此在其调用lookUpImpOrForward时不会触发 消息的解析,仅仅是从“类、父类、...、根类”的缓存中和方法列表中找IMP,没找到会触发 消息转发

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;
}

这里会判断IMP是否是消息转发而来的,如果是,就不返回。

1.3 类方法解析

类方法的解析首先是调用resolveClassMethod函数,其源码如下:

// 这里的cls是元类,因为类方法存储在元类
static void resolveClassMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // 如果你没有实现类方法 +(BOOL)resolveClassMethod:(SEL)sel
        // NSObject也有实现,所以一般不会走这里
        // 注意这里的第一个参数是cls,是元类
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        // 获取 元类的对象,即类。换句话说,nonmeta 也就是 inst
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 调用类方法: +(BOOL)resolveClassMethod:(SEL)sel
    bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);

    // 再找一次imp
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

你会发现,这个函数与resolveInstanceMethod函数大体相同,需要留意的是,这次判断类(包括其父类,直至根类)是否实现的是+(BOOL)resolveClassMethod:(SEL)sel类方法。

让我们回顾一下resolveMethod函数对类方法的解析

// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst, 
        NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
{
    // 此时的cls为元类,也就是 NSObject 调用 resolveInstanceMethod:
    resolveInstanceMethod(cls, sel, inst);
}

在经过resolveClassMethod的处理之后,如果依然没有找到类方法的IMP,就会再次执行resolveInstanceMethod函数!不同于实例方法的是,此时的cls是元类,因此msg(cls, SEL_resolveInstanceMethod, sel);即是向元类内部发送resolveInstanceMethod:消息,也就意味着是根类调用resolveInstanceMethod:方法(这次只能在根类的分类中补救了),同时缓存查找类方法的IMP仅发生在根元类和根类中,而方法列表中查找类方法的IMP则分别在“元类、元类的父类、...、根元类、根类”中进行。

简而言之,当我们调用一个类方法时,如果在类中没有实现,同时在resolveClassMethod中也没有处理,那么最终会调用根类(NSObject)的同名实例方法

1.4 举个栗子

通过上述的分析,相信大家对方法的解析有了一定的认知,下面我们来整个简单的例子消化一下。

@interface Person : NSObject

+ (void)personClassMethod1;
- (void)personInstanceMethod1;

@end

@implementation Person

@end

一个简单的Person类,里面分别有一个类方法和一个实例方法,但是都没有实现。

接着添加对这两个方法的解析:

- (void)unimplementedMethod:(SEL)sel {
    NSLog(@"没实现?没关系,绝不崩溃");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"动态实例方法解析:%@", NSStringFromSelector(sel));
    if (sel == @selector(personInstanceMethod1)) {
        IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
        Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
        const char *methodType = method_getTypeEncoding(method);
        return class_addMethod(Person.class, sel, methodIMP, methodType);
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"动态类方法解析:%@", NSStringFromSelector(sel));
    if (sel == @selector(personClassMethod1)) {
        IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
        Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
        const char *methodType = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("Person"), sel, methodIMP, methodType);
    }
    return [super resolveClassMethod:sel];
}

看看打印:

通过对类方法解析的源码分析,我们知道,也可以把对Person类方法的处理放在NSObject分类的resolveClassMethod:resolveInstanceMethod:中,都能达到相同的效果(记得把Person类中的resolveClassMethod:处理去掉)。这里略过不提。

2 消息转发

方法的调用经过了查找、解析,如果还是没有找到IMP,就会来到消息转发流程。它的入口在lookUpImpOrForward函数靠后的位置

// No implementation found, and method resolver didn't help.     
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

2.1 消息的转发起始和结束

_objc_msgForward_impcache是汇编函数,以arm64架构为例,其源码如下:

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache

__objc_msgForward_impcache内部调用了__objc_msgForward

ENTRY __objc_msgForward

adrp	x17, __objc_forward_handler@PAGE
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
	
END_ENTRY __objc_msgForward

这个函数主要做的事情是,通过页地址与页地址偏移的方式,拿到_objc_forward_handler函数的地址并调用。

说明:

  • adrp是以页为单位的大范围的地址读取指令,这里的p就是page的意思
  • ldr类似与movmvn,当立即数(__objc_msgForward中是[x17, __objc_forward_handler@PAGEOFF]PAGEOFF是页地址偏移值)大于movmvn能操作的最大数时,就使用ldr

OBJC2中,_objc_forward_handler实际上就是objc_defaultForwardHandler函数,其源码如下:

// Default forward handler halts the process.
__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

是不是很熟悉?当我们调用一个没实现的方法时,报的错就是“unrecognized selector sent to ...”

但是问题来了,说好的消息转发流程呢?这才刚开始怎么就结束了?不急,憋慌,且看下去。

2.2 消息转发的调用栈

回顾方法解析时举的例子,不妨把解析的内容去掉,Let it crash!

发现在崩溃之前与消息转发相关的内容是,调用了_CF_forwarding_prep_0___forwarding___这两个函数。遗憾的是这两个函数并未开源。

既然崩溃信息不能提供帮助,只好打印具体的调用信息了。

在方法的查找流程中,log_and_fill_cache函数就跟打印有关,跟踪其源码:

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill(cls, sel, imp, receiver);
}

bool objcMsgLogEnabled = false;

// Define SUPPORT_MESSAGE_LOGGING to enable NSObjCMessageLoggingEnabled
#if !TARGET_OS_OSX
#   define SUPPORT_MESSAGE_LOGGING 0
#else
#   define SUPPORT_MESSAGE_LOGGING 1
#endif

打印的关键函数就是logMessageSend,但是它受SUPPORT_MESSAGE_LOGGINGobjcMsgLogEnabled控制。

继续跟进SUPPORT_MESSAGE_LOGGING

#if !DYNAMIC_TARGETS_ENABLED
    #define TARGET_OS_OSX               1
    ...
#endif
    
#ifndef DYNAMIC_TARGETS_ENABLED
 #define DYNAMIC_TARGETS_ENABLED   0
#endif

从源码不难看出TARGET_OS_OSX的值是1,因此,SUPPORT_MESSAGE_LOGGING也为1!

如果能把objcMsgLogEnabled改成true,显然就可以打印调用信息了。通过全局搜索objcMsgLogEnabled,我们找到了instrumentObjcMessageSends这个关键函数

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;
}

接下来就好办了!来到main.m,添加以下代码

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        instrumentObjcMessageSends(true);
        [Person personClassMethod1];
        instrumentObjcMessageSends(false);
    }
    return 0;
}

运行工程,直到再次崩溃。此时已打印函数调用栈,日志文件位置在logMessageSend函数中有标注

    // 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;
        }
    }

打开Finder(访达),cmd + shift + G快捷键,输入/tmp/msgSends,找到最新的一份日志文件(数字最大)

打印结果如下:

从这份日志可以看出,与转发相关的方法是forwardingTargetForSelectormethodSignatureForSelector,分别对应了消息的快速转发流程和慢速转发流程,接下来开始分析这两个方法。

2.3 消息的快速转发

forwardingTargetForSelector:对应的就是消息的快速转发流程,它在源码中只是简单的返回nil(可在子类或分类中重写)

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

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

不过我们可以在开发文档中找到说明(cmd + shift + 0快捷键)

概括地说,forwardingTargetForSelector:主要是返回一个新的receiver,去处理sel这个当前类无法处理的消息,如果处理不了,会转到效率低下的forwardInvocation:。在效率方面,forwardingTargetForSelector:领先forwardInvocation:一个数量级,因此,最好不要用后者的方式处理消息的转发逻辑。

关于forwardingTargetForSelector:返回的新的receiver,需要注意一下几点:

  • 绝对不能返回self,否则会陷入无限循环;
  • 不处理的话,可以返回nil,或者[super forwardingTargetForSelector:sel](非根类的情况),此时会走methodSignatureForSelector:慢速转发流程;
  • 如果有这个receiver,此时相当于执行objc_msgSend(newReceiver, sel, ...),那么它必须拥有和被调用的方法相同方法签名的方法(方法名、参数列表、返回值类型都必须一致)。

2.3.1 举个栗子

我们可以实验一下,准备工作如下

@interface ForwardObject : NSObject

@end

@implementation ForwardObject

+ (void)personClassMethod1 {
    NSLog(@"类方法转发给%@,执行%s", [self className], __FUNCTION__);
}

- (void)personInstanceMethod1 {
    NSLog(@"实例方法转发给%@,执行%s", [self className], __FUNCTION__);
}

@end

@interface Person : NSObject

+ (void)personClassMethod1;
- (void)personInstanceMethod1;

@end

@implementation Person

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"实例方法开始转发");
    return [ForwardObject alloc];
}

+ (id)forwardingTargetForSelector:(SEL)sel {
    NSLog(@"类方法开始转发");
    return [ForwardObject class];
}

@end

显然,ForwardObject作为消息转发后的处理类,拥有Person类的同名类方法和实例方法。现在开始验证,结果如下:

事实证明确实有效!接下来看消息的慢速转发流程。

2.4 消息的慢速转发

如果forwardingTargetForSelector:没有处理消息(如返回nil),就会启动慢速转发流程,也就是methodSignatureForSelector:方法,同样需要在子类或分类中重写

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

通过阅读官方文档,我们得出以下结论:

  • methodSignatureForSelector:方法是跟forwardInvocation:方法搭配使用的,前者需要我们根据sel返回一个方法签名,后者会把这个方法签名封装成一个NSInvocation对象,并将其作为形参。
  • 如果有目标对象能处理Invocation中的selInvocation可以指派这个对象处理;否则不处理。
    • Invocation可以指派多个对象处理

注意:消息的慢速转发流程性能较低,如果可以的话,你应该尽可能早地处理掉消息(如在方法解析时,或在消息的快速转发流程时)。

2.4.1 举个栗子

针对慢速流程,同样可以验证。这里把快速转发例子中的Person类修改一下:

@implementation Person

// MARK: 慢速转发--类方法

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"类方法慢速转发:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(personClassMethod1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = [anInvocation selector];
    NSLog(@"类方法慢速转发:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    id target = [ForwardObject class];
    if ([target respondsToSelector:aSelector]) [anInvocation invokeWithTarget:target];
    else [super forwardInvocation:anInvocation];
}

// MARK: 慢速转发--实例方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"实例方法慢速转发:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(personInstanceMethod1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = [anInvocation selector];
    NSLog(@"实例方法慢速转发:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    ForwardObject *obj = [ForwardObject alloc];
    if ([obj respondsToSelector:aSelector]) [anInvocation invokeWithTarget:obj];
    else [super forwardInvocation:anInvocation];
}

@end

其结果如下图所示,显然也没有崩溃。

对方法签名类型编码不熟悉的可以查看 苹果官方的类型编码介绍

3 总结

综上所述,当我们调用方法时,首先进行方法的查找,如果查找失败,会进行方法的解析,此时OC会给我们一次对sel的处理机会,你可以在resolveInstanceMethod:(类方法对应resolveClassMethod:)中添加一个IMP;如果你没把握住这次机会,也就是解析失败时,会来到消息转发阶段,这个阶段有两个机会去处理sel,分别是快速转发的forwardingTargetForSelector:,以及慢速转发的methodSignatureForSelector:。当然,如果这些机会你都放弃了,那OC只好让程序崩溃。

下面用一副图总结方法的解析和转发流程

4 问题讨论

4.1 为什么引入消息转发机制?

在一个方法被调用之前,我们是没办法确定它的实现地址的,直到运行时,这个方法被调用的时候,我们才能真正知道它是否有实现,以及其具体的实现地址。这也就是所谓的“动态绑定”。

在编译期,如果编译器发现方法不存在,会直接报错;同样,在运行时,也有doesNotRecognizeSelector的处理。

在抛出doesNotRecognizeSelector这个异常信息之前,OC利用其动态绑定的特性,引入了消息转发机制,给予了我们额外的机会处理消息(解析 or 转发),这样的做法显然更加周全合理。

5 PS