在OC中,所有的方法调用其实都是通过消息传递机制来查询且执行方法,而且在编译期间,是不知道谁调用谁的,都是在运行时才确定调用对象,而消息传递的核心则是
objc_msgSend
函数,下面我们就开始研究下objc_msgSend
我们在开发的时候最常见的就是方法的调用了,但是在OC中我们一般都看到函数调用这个样子的 [receiver hello:@"word"]
, 但是这个方法会被编译器转换成 objc_msgSend(receiver,@selector(hello:),@"word")
我们来看下id objc_msgSend(id self, SEL op, ...)
这个函数,它其实主要做了下面三件事情:
- 检测这个 selector是不是要忽略的。
- 检查target是不是为nil。(如果这里有相应的nil的处理函数,就跳转到相应的函数中。如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因)
- 确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现。(如果找到,就跳转进去执行。如果没有找到,就在方法分发表里面继续查找,一直找到NSObject为止。)
- 如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程。
我们在objc4-706中的objc-msg-x86_64.s
中找到这么一段汇编代码:
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in r11
* Forwarding returned in Z flag
* r10 reserved for our use but not used
*
********************************************************************/
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
.fill 256, 8, 0
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
ENTRY _objc_msgLookup
NilTest NORMAL
GetIsaFast NORMAL // r10 = self->isa
CacheLookup NORMAL, LOOKUP // returns IMP on success
NilTestReturnIMP NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r10
jmp __objc_msgLookup_uncached
END_ENTRY _objc_msgLookup
ENTRY _objc_msgSend_fixup
int3
END_ENTRY _objc_msgSend_fixup
STATIC_ENTRY _objc_msgSend_fixedup
// Load _cmd from the message_ref
movq 8(%a2), %a2
jmp _objc_msgSend
END_ENTRY _objc_msgSend_fixedup
/********************************************************************
*
* _objc_msgSend_uncached
* _objc_msgSend_stret_uncached
* _objc_msgLookup_uncached
* _objc_msgLookup_stret_uncached
*
* The uncached method lookup.
*
********************************************************************/
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
STATIC_ENTRY __objc_msgSend_stret_uncached
UNWIND __objc_msgSend_stret_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 STRET // r11 = IMP
jmp *%r11 // goto *imp
END_ENTRY __objc_msgSend_stret_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_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
ret
END_ENTRY __objc_msgLookup_uncached
STATIC_ENTRY __objc_msgLookup_stret_uncached
UNWIND __objc_msgLookup_stret_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 STRET // r11 = IMP
ret
END_ENTRY __objc_msgLookup_stret_uncached
这两段代码无非就是做了两件事, CacheLookup 和 MethodTableLookup,有缓存的方法查找,如果cache缓存中获取失败,就从方法列表中查找
然后我们看一下查找方法的方法源码:
/***********************************************************************
* getMethodNoSuper_nolock
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
在search_method_list函数中,会去判断当前methodList是否有序,如果有序,会调用findMethodInSortedMethodList方法,这个方法里面的实现是一个二分搜索。如果非有序,就调用线性的遍历搜索。
如果直到NSObject还没有找到方法的实现的话,就会进入下面的消息转发:
+(BOOL)resolveInstanceMethod:(SEL)sel
和+(BOOL)resolveClassMethod:(SEL)sel
这一阶段我们可以为类添加方法void dynamicMethodIMP(id self, SEL _cmd){ NSLog(@"%s", __PRETTY_FUNCTION__); } + (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(hello)) { class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "V@:"); return YES; } return [super resolveInstanceMethod:sel]; }
- (id)forwardingTargetForSelector:(SEL)aSelector
这一步可以换一个消息接受者处理消息- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(Method:)){ return otherObject; } return [super forwardingTargetForSelector:aSelector]; }
如果以上两步还未处理消息,就只能启用完整的消息转发机制了
- (void)forwardInvocation:(NSInvocation *)anInvocation
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation 方法中选择将消息转发给其它对象。还有一个很重要的问题,我们必须重写以下方法:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SomeObject instancesRespondToSelector:aSelector]) {
signature = [SomeObject instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SomeObject instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget: SomeObject];
} else {
[super forwardInvocation:anInvocation];
}
}
我们来看一下消息发送和转发的过程:
研究完了消息转发,我们会有一个问题,为什么objc_msgSend必须用汇编实现,而不是C/C++或者Objective-C ?
我们先来看下两行代码:
NSUInteger length = [string length];
NSString *str = [string substringFromIndex:2];
将会被编译器翻译成:
NSUInteger length = objc_msgSend(string, @selector(length));
NSString *str = objc_msgSend(string, @selector(substringFromIndex:), 2);
但是实际上这是不可能的,因为没有函数可以同时满足这两个调用。而且它的返回值也不能同时是NSUInteger和NSString。
而且,上面的代码也是无法编译通过的。那么,加上类型转换怎么样?
NSUInteger length = ((NSUInteger (*)(id, SEL))objc_msgSend)(string, @selector(length));
NSString *str = ((NSString *(*)(NSString *, SEL, NSUInteger))objc_msgSend)(string, @selector(substringFromIndex:), 2);
这下可以编译通过了,objc_msgSend是一个Public的函数,在<objc/message.h>里声明,如果你想直接调用它,就必须按照上面的格式加上强制类型转换,要不然是无法编译通过的。但是objc_msgSend到底是如何实现,来支持各种返回类型的?
- 参数类型和数量
当objc_msgSend找到对应的函数指针后,只要用传入的参数调用这个函数即可。剩下来的就是找到一种方法,可以调用任意参数类型、数量的任意函数。
参数的数量很容易计算。然后我们可以把所有的参数都放入varargs,然后调用函数时传入即可。但是这样的话,每个Objective-C的方法都必须在其prologue(译者注:函数执行具体的“任务”前,所做的准备环节)里面把所有的参数从varargs里面提取出来。这种把参数打包到varargs里面然后又取出来的办法显然是非常糟糕的,同时也是不必要的。
在C语言中,调用一个函数会被编译成对应的汇编语言指令,首先是设置参数(把参数放到寄存器、栈上),然后用如jump或者call的指令,跳到具体的函数代码地址处。如果我们想支持任意类型的函数类型,我们就必须写一个switch语句,把所有的参数组合情况都包含起来,这样才能正确的为任何形式的函数设置参数(译者注:即按照某种“规范”、“约定”,把参数依次存放到“约定”的寄存器、栈上),这显然是没有扩展性的,更是不可能的。
- 拆解调用
objc_msgSend
的解决办法,主要依据的是:当objc_msgSend被调用时,所有的参数已经被设置好了。
换一种方式来说,就是:在objc_msgSend开始执行时,栈帧(stack frame)的状态、数据,和各个寄存器的组合形式、数据,跟调用具体的函数指针(IMP)时所需的状态、数据,是完全一致的!
如下这行代码:
NSString *str = ((NSString *(*)(NSString *, SEL, NSUInteger))objc_msgSend)(string, @selector(substringFromIndex:), 2);
在调用objc_msgSend时,需要设置三个参数,分别是被调用方receiver、方法名selector和最后一个整型参数2。这和具体的方法函数IMP的参数顺序、类型是完全一致的,也就是说,调用objc_msgSend前,设置的栈、寄存器的状态、数据正是调用具体的方法函数时需要的状态!
所以,当objc_msgSend找到要调用的函数实现IMP后,只需要把所有的对栈、寄存器的操作“倒”回到objc_msgSend执行开始的状态(类似于函数执行完成return返回前,做的“收尾处理”工作一样,即epilogue),直接jump/call到IMP函数指针对应的地址,执行指令即可,因为所有的参数已经被设置好了。
同时,当selector对应的IMP执行完成后,返回值也被正确的设置好了(在x86平台上,返回值被设置到了指定的寄存器eax/rax里,在arm上,则是r0寄存器),所以,我们也不必担心前文提到的不同类型的返回值问题了。
- 总结
在C语言里面调用函数,必须在编译时就知道调用的“状态”;而这些“状态”在运行时是无法得出或正确处理的,所以必须往底层走,用汇编处理。有人指出objc_msgSend
有可能是用GCC的扩展方法__builtin_apply_args,__builtin_apply,和__builtin_return
实现的。这也正指出了一个事实,就是这些builtins方法是非常有必要的,因为单靠语言本身无法实现这些功能。实现objc_msgSend
所需要的技巧,也正是实现这些builtins方法所需要的技巧。
用汇编实现,是为了应对不同的“Calling convention”,把函数调用前的栈和寄存器的参数、状态设置,交给编译器去处理。
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/bf9a09e9.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!