Objective-C 消息转发源码解析

Objective-C 消息转发源码解析

官方消息转发文档:
Message Forwarding

消息转发机制是为了留给用户一些处理异常的余地。从 objc 层面我们看到的是这几个oc 方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel, - (id)forwardingTargetForSelector:(SEL)aSelector, - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector, - (void)forwardInvocation:(NSInvocation *)anInvocation

objc 是一个完全基于运行时的系统,因此真正执行的代码还是 c 函数和 c 结构,为此我找来了 objc 的开源代码,为大家揭秘消息转发的底层实现。

整个消息转发的源头开始于, objc_msgSend 消息发送函数,由于消息的转发和消息的函数实现有着密切关系,所以我们只需要聚焦在系统如何对 IMP 函数实现进行查找的过程中,其他逻辑暂时不去关注。objc_msgSend 调用 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls),而 _class_lookupMethodAndLoadCache3 调用 lookUpImpOrForward,然后调用 cache_getImp

_class_lookupMethodAndLoadCache3 通过 lookUpImpOrForward 查找实现或转发消息

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
lookUpImpOrForward 的作用是查找目标 imp ,如果找到则输出日志并加入缓存,缓存 cache_t 的实现是一个链式哈希表(可以在 objc-runtime-new.h 中找到声明,实现在 objc-cache.mm 中),如果没有则返回 _objc_msgForward_impcache 消息转发函数

+ resolveInstanceMethod: 该方法的目的是给用户一个机会来添加消息对应的实现。除此外无其他逻辑。

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) 函数,会通过 resolveInstanceMethod 让用户知道runtime 在解析该方法,如果有需要可以在这时添加方法的实现。

_objc_msgForward_impcache, __objc_msgForward, __objc_forward_handler, 最后由 forward_handler 发起了 - forwardInvocation: 调用

至此开始消息转发之前的逻辑,已经介绍完毕,最重要的转发逻辑都在 __objc_forward_handler 中,关于这个函数的实现,从 objc 源码中只能看到汇编级的函数调用,可见 __objc_forward_handler 的源码并不在 objc 中实现。

但是根据断点,我们可以推断,在 __objc_forward_handler中分别处理了以下几个方法的回调过程,并且最后通过 forwardInvocation 调用了方法的实现 。

- forwardingTargetForSelector:

- methodSignatureForSelector:

- forwardInvocation:

关于这个转发我们可以参考这篇文章: What’s that Selector? 解释的非常详尽。

objc_msgSend

id objc_msgSend(id self, SEL _cmd,…);

通过以下的汇编代码,我们可以看到 CacheLookup NORMAL, CALL // calls IMP on success,CacheLookup 对缓存进行查找,如果查找成功就直接调用,查找失败就跳转到 __objc_msgLookup_uncached (该方法我们通过在 + (BOOL)resolveInstanceMethod:(SEL)sel 中添加断点可以看到),然后调用 MethodTableLookup,继而调用 _class_lookupMethodAndLoadCache3,在调用 lookUpImpOrForward, 通过 cache_getImp 查找缓存中的 imp ,查到后通过 log_and_fill_cache(cls, imp, sel, inst, curClass) 将 imp 缓存起来,如果查不到则进行消息转发, static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)

.macro CacheLookup

_objc_msgSend_uncached

对于未缓存的方法,运行时会进行方法搜索
__objc_msgSend_uncached

/********************************************************************
 *
 * _objc_msgSend_uncached
 * _objc_msgSend_stret_uncached
 * _objc_msgLookup_uncached
 * _objc_msgLookup_stret_uncached
 *
 * The uncached method lookup.
 *
 ********************************************************************/

.macro MethodTableLookup

这一段代码是用汇编实现的宏,它会调用 _class_lookupMethodAndLoadCache3 函数查找方法并加载缓存

_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)

该函数 通过 lookUpImpOrForward 查找方法实现和消息转发

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)

/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
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;
}

lookUpImOrNil 通过 lookUpImpOrForward 查找方法实现,如果查找的方法实现是 _objc_msgForward_impcache 默认的消息转发函数,则直接返回 nil 以忽略消息转发。从这里可以看到 C 编程中的复用设计思想,通过对函数的分层封装达到不同层完成不同功能的作用。

lookUpImpOrForward

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)

resolver 参数决定是否进行消息转发,对于 lookUpImpOrNil 函数的实现其实是通过设置 resolver 为 NO 来调用 lookUpImpOrForward 完成的。

通过 cache_getImp 查找对应的 imp 实现,如果查找到则通过 log_and_fill_cache 输出日志并加入缓存,直接退出 lookUpImpOrForward 函数。

如果未找到 imp 实现,则通过调用 _class_resolveMethod 通知用户是否能够解析该方法,让用户做出响应处理,在 resolve 过程中会依次向上遍历当前类的所有父类是否有该方法的实现(该过程分为两步,第一步是从缓存中查找实现,第二步通过 runtime 查找 Method 实例并获取 imp 实现同时加入缓存。),如果没有则继续向上遍历直到根类,如果找到则结束当前函数。然后,goto retry: ,再次尝试 cache_getImp,不论用户处理如何, 下一步会通过 _objc_msgForward_impcache 进行消息转发。

__objc_msgForward_stret 是用汇编实现的,根据情况有选择的分别调用 __objc_msgForward_stret__objc_msgForward 函数。

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass; //指向当前处理类
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO; //已经尝试过重解析

    rwlock_assert_unlocked(&runtimeLock); //确保运行时已解锁

    // Optimistic cache lookup
    if (cache) {//允许查找 imp 缓存
        imp = cache_getImp(cls, sel); //根据 selector 查找目标类 cls
        if (imp) return imp; //命中则直接返回
    }

    if (!cls->isRealized()) {
        rwlock_write(&runtimeLock);
        realizeClass(cls); //识别 cls 类,需要调用方自行进行加锁操作。该函数建立了类的继承关系以及 objc 中的类别。
        rwlock_unlock_write(&runtimeLock);
    }

    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    // be added but ignored indefinitely because the cache was re-filled 
    // with the old value after the cache flush on behalf of the category.
 retry:
    rwlock_read(&runtimeLock);

    // Ignore GC selectors
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp); // imp 缓存的实现是基于 cache_t 的链式哈希表,key 是 (cache_key_t)sel,value 是 imp。
        goto done;
    }

    // Try this class's cache.

    imp = cache_getImp(cls, sel); //cache_getImp 为汇编开发的查表函数 IMP cache_getImp(Class cls, SEL sel)。虽然看不到直观的实现逻辑,但是能够推断,它是依赖哈希表的机制进行 imp 查找,使用汇编开发也是为了提高 imp 的查找效率。
    if (imp) goto done; //查到 imp 则结束当前函数。

    // Try this class's method lists.

    meth = getMethodNoSuper_nolock(cls, sel); //如果查不到 imp 则对方法列表进行查找,通过 Method 找到 imp。
    if (meth) {
        log_and_fill_cache(cls, cls, meth->imp, sel); // 输出日志并加入缓存,这里的缓存前文已经介绍过,是通过 cache_fill -> cache_fill_nolock 以链式哈希表实现的缓存处理,在 cache_fill_nolock 的实现中通过 getCache 函数的获取当前类的 imp 缓存,换句话说每个类都有自己的 imp 缓存。
        imp = meth->imp;
        goto done;
    }

    // Try superclass caches and method lists.

    curClass = cls;
    // 继续查找父类的 imp 实现
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, curClass, imp, sel);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            // 将已找到的 imp 实现和目标类的选择器在 imp 缓存中关联,下次再访问方法,就直接调用父类实现,换句话说,如果子类没有实现方法,则调用父类的方法实现,实现了 objc 的继承机制。
            log_and_fill_cache(cls, curClass, meth->imp, sel);
            imp = meth->imp;
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        rwlock_unlock_read(&runtimeLock);
        // 如果还未冲解析,则通知用户进行方法解析,这里用到了“模板方法”设计模式。将可变的部分交给用户来实现,系统本身的核心逻辑不受影响。
        _class_resolveMethod(cls, sel, inst);
        // 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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    // _objc_msgForward_impcache 是默认的 ForwardHandler 实现,这个处理器主要负责了最重要的消息转发过程以及我们熟知的 `forwardingTargetForSelector`, `methodSignatureForSelector`, `forwardInvocation`, `doesNotRecognizeSelector` 方法调用控制。目前从苹果公开的源码来看,这是一个黑盒。 从源码中可以发现 _objc_msgForward_impcache 调用了 `__objc_msgForward_stret` 和 `__objc_msgForward`。
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp);// 同时将该处理器加入缓存,下次再发送该消息的时候,直接调用该转发处理器。

 done:
    rwlock_unlock_read(&runtimeLock);

    // paranoia: look for ignored selectors with non-ignored implementations
    assert(!(ignoreSelector(sel)  &&  imp != (IMP)&_objc_ignored_method));

    // paranoia: never let uncached leak out
    assert(imp != _objc_msgSend_uncached_impcache);

    return imp;
}

_class_resolveMethod

void _class_resolveMethod(Class cls, SEL sel, id inst)

runtime 通过 _class_resolveMethod 进行方解析,如果 cls 是元类,则调用 _class_resolveClassMethod,然后继续用 lookUpImpOrNil 查找方法实现,否,则查找元类的实例方法(元类的实例,就是类。类的实例是对象。元类是用来描述类的。);否则调用 _class_resolveInstanceMethod

可以认为它相当于 + resolveInstanceMethod 的 c 实现

“`objc-class.mm
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
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_resolveClassMethod > static void _class_resolveClassMethod(Class cls, SEL sel, id inst) > > 负责解析类方法

/***********************************************************************
* _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 doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

//... 省略日志

}



#### _class_resolveInstanceMethod > static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) > > 专门负责解析实例方法,如果 lookUpImpOrNil 查找失败,则直接返回。 > > `bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);` 这一行,会执行实例的 `resolveInstanceMethod` 重载方法,返回结果就是 `resolved` ,通过 `lookUpImpOrNil` 函数查找对应的 imp 实现。 > > 从 `if (resolved && PrintResolving) {}` 这段代码可以看到 resolved 只对输出日志产生影响,并不影响消息转发逻辑。

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

// msg 就是 objc_msgSend 这里直接向类发送了消息。
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 doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

//....省略日志代码

}


#### CacheLookup > CacheLookup

.macro CacheLookup
.if $0 != STRET
movq %a2, %r11 // r11 = _cmd
.else
movq %a3, %r11 // r11 = _cmd
.endif
andl 24(%r10), %r11d // r11 = _cmd & class->cache.mask
shlq $$4, %r11 // r11 = offset = (_cmd & mask)< <4
addq 16(%r10), %r11 // r11 = class->cache.buckets + offset

.if $0 != STRET
cmpq (%r11), %a2 // if (bucket->sel != _cmd)
.else
cmpq (%r11), %a3 // if (bucket->sel != _cmd)
.endif
jne 1f // scan more
// CacheHit must always be preceded by a not-taken jne instruction
CacheHit $0, $1 // call or return imp

1:
// loop
cmpq $$1, (%r11)
jbe 3f // if (bucket->sel < = 1) wrap or miss

addq    $$16, %r11      // bucket++

2:
.if $0 != STRET
cmpq (%r11), %a2 // if (bucket->sel != _cmd)
.else
cmpq (%r11), %a3 // if (bucket->sel != _cmd)
.endif
jne 1b // scan more
// CacheHit must always be preceded by a not-taken jne instruction
CacheHit $0, $1 // call or return imp

3:
// wrap or miss
jb LCacheMiss_f // if (bucket->sel < 1) cache miss
// wrap
movq 8(%r11), %r11 // bucket->imp is really first bucket
jmp 2f

// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.

1:
// loop
cmpq $$1, (%r11)
jbe 3f // if (bucket->sel < = 1) wrap or miss

addq    $$16, %r11      // bucket++

2:
.if $0 != STRET
cmpq (%r11), %a2 // if (bucket->sel != _cmd)
.else
cmpq (%r11), %a3 // if (bucket->sel != _cmd)
.endif
jne 1b // scan more
// CacheHit must always be preceded by a not-taken jne instruction
CacheHit $0, $1 // call or return imp

3:
// double wrap or miss
jmp LCacheMiss_f

.endmacro
“`

总结

Objective-C 消息发送过程包含,查找消息实现,实现调用,和消息转发三个主要过程。

Objective-C 消息发送与转发机制原理 | yulingtianxia’s blog