在最新的 Objective-C 运行时中,objc_msgSend
的声明是这样的
/* Basic Messaging Primitives
*
* On some architectures, use objc_msgSend_stret for some struct return types.
* On some architectures, use objc_msgSend_fpret for some float return types.
* On some architectures, use objc_msgSend_fp2ret for some float return types.
*
* These functions must be cast to an appropriate function pointer type
* before being called.
*/
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
可以看到文档注释上说明的,还有 objc_msgSend_stret
、objc_msgSend_fpret
和 objc_msgSend_fp2ret
来应对特殊返类型,但我们重点关注 objc_msgSend
本身。
文档注释里还写到它在调用前需要转换成合适的类型,在 performSelector:
方法中可以看到这种用法
- (id)performSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}
- (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
- (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2);
}
可以看到这三个方法都根据自己的需要将 objc_msgSend
转换成了对应的类型。
objc_msgSend
是用汇编实现的,除了用汇编速度快以外,这篇文章也列出了一些别的原因。
这里只对 x86-64 架构的汇编进行一点研究,代码可以在 objc-msg-x86_64.s
中找到
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r10 = self->isa
CacheLookup NORMAL, CALL // calls IMP on success
NilTestReturnZero NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r10
MESSENGER_END_SLOW
jmp __objc_msgSend_uncached
END_ENTRY _objc_msgSend
因为我对汇编实在不熟,就随便看看吧……
首先可以看到 NilTest
,这应该就是对 nil
的检测,这样的话对 nil
发消息便不会奔溃并且会返回 nil
。紧接着 GetIsaFast
,就照着它名字的意思,快快的拿到 isa
,好用来干下面的事情。拿到 isa
后,先检查缓存中是否已经缓存了方法,如果缓存了就调用并返回,这就是 CacheLookup
干的事情,如果缓存里没有,那就跳到 LCacheMiss
标签,接着调用 __objc_msgSend_uncached
。
继续看到 __objc_msgSend_uncached
,它也是用汇编写的
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r10 is the searched class
// r10 is already the class to search
MethodTableLookup NORMAL // r11 = IMP
jmp *%r11 // goto *imp
END_ENTRY __objc_msgSend_uncached
这就干了一件事,MethodTableLookup
找到方法,跳转到 IMP
。继续看看 MethodTableLookup
这个宏
.macro MethodTableLookup
push %rbp
mov %rsp, %rbp
sub $$0x80+8, %rsp // +8 for alignment
movdqa %xmm0, -0x80(%rbp)
push %rax // might be xmm parameter count
movdqa %xmm1, -0x70(%rbp)
push %a1
movdqa %xmm2, -0x60(%rbp)
push %a2
movdqa %xmm3, -0x50(%rbp)
push %a3
movdqa %xmm4, -0x40(%rbp)
push %a4
movdqa %xmm5, -0x30(%rbp)
push %a5
movdqa %xmm6, -0x20(%rbp)
push %a6
movdqa %xmm7, -0x10(%rbp)
// _class_lookupMethodAndLoadCache3(receiver, selector, class)
.if $0 == NORMAL
// receiver already in a1
// selector already in a2
.else
movq %a2, %a1
movq %a3, %a2
.endif
movq %r10, %a3
call __class_lookupMethodAndLoadCache3
// IMP is now in %rax
movq %rax, %r11
movdqa -0x80(%rbp), %xmm0
pop %a6
movdqa -0x70(%rbp), %xmm1
pop %a5
movdqa -0x60(%rbp), %xmm2
pop %a4
movdqa -0x50(%rbp), %xmm3
pop %a3
movdqa -0x40(%rbp), %xmm4
pop %a2
movdqa -0x30(%rbp), %xmm5
pop %a1
movdqa -0x20(%rbp), %xmm6
pop %rax
movdqa -0x10(%rbp), %xmm7
.if $0 == NORMAL
cmp %r11, %r11 // set eq for nonstret forwarding
.else
test %r11, %r11 // set ne for stret forwarding
.endif
leave
.endmacro
其实这个宏最重要的就是 call __class_lookupMethodAndLoadCache3
这个语句了,它上面和下面的语句大多都是对参入的入栈出栈操作。那查找方法的任务其实就交给 _class_lookupMethodAndLoadCache3
这个函数了,在 objc-runtime-new.mm
中可以看到它的实现
/***********************************************************************
* _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*/);
}
从文档注释可以得知,这个函数是专门给 objc_msgSend
用的,它让 lookUpImpOrForward
函数减少一次缓存查找,因为之前在 objc_msgSend
中已经查找过一次了。继续看看 lookUpImpOrForward
函数
/***********************************************************************
* 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;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}
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:
runtimeLock.read();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// Try superclass caches and method lists.
curClass = cls;
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, imp, sel, inst, curClass);
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) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_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.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
这么一大坨实在太多了,我们分开来读读这段代码
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
首先进行初始化,curClass
用来存放正在查找的类,imp
用来存放找到的 IMP
,meth
用来存放找到的 Method
,triedResolver
用来表示是否进行过动态方法解析。
runtimeLock.assertUnlocked();
在 DEBUG
模式下 unlock assert
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
进行一次无锁的缓存查找,从 objc_msgSend
进来时,cache
被设置成了 false
,所以这一步会跳过,原因是 objc_msgSend
里已经进行过了无锁的缓存查找了
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}
这里对判断类是否载入了运行时,如果没有的话,会进行载入,其中会调用熟知的 +load
方法
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
}
这里判断类是否进行了初始化,也就 +initialize
方法是否调用过。
接下来就是重头戏,方法的查找了
// 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:
runtimeLock.read();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
首先给读加个锁,防止在读取中有数据写入(有 category 加入),之后再次尝试从缓存中查找,因为加锁前可能已经有别的地方将方法的缓存加入了,如果缓存中还是没有的话,继续下面的过程
// Try this class's method lists.
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// Try superclass caches and method lists.
curClass = cls;
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, imp, sel, inst, curClass);
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) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
首先使用 getMethodNoSuper_nolock(cls, sel)
在类里查找方法,如果找到了,调用 log_and_fill_cache
函数填充缓存并跳到 done
。接下来如果在类上没有找到的话,就往父类查找,首先现在父类的缓存中查找,找到后需要检查是否是 _objc_msgForward_impcache
,这是运行时中消息转发的实现,需要先无视父类中消息转发的缓存,因为子类里可能实现了动态方法解析。如果父类的缓存中没有,就调用 getMethodNoSuper_nolock
在父类中查找方法,找到了就跳到 done
,没有就继续这个过程。
如果最后都没有找到实现,那就进入动态方法解析过程
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_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;
}
首先先判断是否开启了动态方法解析并且还没有尝试过,之后将锁打开调用 _class_resolveMethod
,这个函数会根据 cls
是否是元类来调用我们熟知的 +resolveInstanceMethod:
或者 +resolveClassMethod
。之后便重新走一遍消息发送过程(goto retry
)。
如果动态方法解析之后还是没有找到方法的实现,便进入消息转发过程
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
objc_msgSend
中消息转发和我们熟悉的有点不太一样,单纯的就是将 _objc_msgForward_impcache
这个函数放到缓存里并返回了。
消息转发
想要弄清楚消息转发是什么情况,继续从 _objc_msgForward_impcache
着手,可以在平台对应的汇编文件中找到它的实现,在这我们就研究一下 x86-64 的代码,在 objc-msg-x86_64.s
中
STATIC_ENTRY __objc_msgForward_impcache
// Method cache version
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band condition register is NE for stret, EQ otherwise.
MESSENGER_START
nop
MESSENGER_END_SLOW
jne __objc_msgForward_stret
jmp __objc_msgForward
END_ENTRY __objc_msgForward_impcache
可以看到根据情况进入 __objc_msgForward_stret
或者 __objc_msgForward
了,继续看它们的实现
ENTRY __objc_msgForward
// Non-stret version
movq __objc_forward_handler(%rip), %r11
jmp *%r11
END_ENTRY __objc_msgForward
ENTRY __objc_msgForward_stret
// Struct-return version
movq __objc_forward_stret_handler(%rip), %r11
jmp *%r11
END_ENTRY __objc_msgForward_stret
它们只是分别调用了 __objc_forward_handler
和 __objc_forward_stret_handler
,我们可以在 objc-runtime.mm
中看到 _objc_forward_handler
的定义和它的默认实现
// 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;
默认实现直接就抛出了错误,没有我们熟知的 forwardingTargetForSelector:
和 forwardInvocation:
。但是可以在定义的下面可以发现一个函数的实现
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
Objective-C 运行时的使用者可以自行设置 forward handler,我们可以在这里下一个断点看看是谁设置了它
可以清楚的看到 __CFInitialize
对运行时的 forward handler 进行了设置,所以 forwardingTargetForSelector:
和 forwardInvocation:
其实是 Core Foundation 所带来的。
这篇文章对消息转发有着更深入的探索。因为 Apple 公开的 Core Foundation 源码中将设置 forward handler 的代码删去了,所以这篇文章还动用了反编译进行探索。
总结
在了解了 objc_msgSend
的原理后,也终于了解了它的一部分具体实现,理清了动态方法解析和消息转发的实际实现位置。动态方法解析是运行时所实现的,消息转发是 Core Foundation 对运行时简单的实现进行了扩充。