的实际操作objc_msgSend函数是我们在Objective-C中实现的所有内容的基础。周五问答读者GwynneRaskind建议我谈谈objc_msgSend的内部结构。有没有比自己动手做更好的理解方式?我们自己实现一个objc_msgSend。蹦床!蹦床!(蹦床)当你写一个发送Objective-C消息的方法时:[objmessage]编译器将生成一个objc_msgSend调用:objc_msgSend(obj,@selector(message));之后由objc_msgSend负责转发消息。它做了什么?它寻找合适的函数指针或IMP,调用它,最后跳转。传递给objc_msgSend的任何参数最终都会成为IMP的参数。IMP的返回值成为最初调用的方法的返回值。因为objcmsgSend只负责接收参数,找到合适的函数指针,跳转,有时也叫trampoline(译注:[trampoline](https://en.wikipedia.org/wiki/Trampoline(computing))。更笼统的说,任何负责将一段代码转发到另一个地方的代码都可以称为蹦床。这种转发行为让objc_msgSend变得特别。因为它只是简单地寻找合适的代码并直接跳转。在过去,这是相当通用的。您可以传入任何参数组合,因为它只是将它们留给IMP读取。返回值有点棘手,但最终都是objc_msgSend的不同变体。不幸的是,这些转发行为无法在纯C.因为没有办法将传入一个C函数的泛型参数传递给另一个函数,可以使用可变参数,但是传递可变参数和普通参数的方法是不同的和缓慢的。所以这不适用于普通的C参数。如果想用C实现objc_msgSend,基本的样子应该是这样的:,...);}这有点过于简单化了。其实会有一个方法缓存来提高查找速度,像这样:(c,_cmd);returnimp(self,_cmd,...);}通常为了速度,cache_lookup是使用内联函数实现的。Assembly在Apple版本的runtime中,为了最大限度地提高速度,整个功能都是使用assembly实现的。在Objective-C中,每次发送消息时都会调用objc_msgSend,应用程序中最简单的操作可能有数千或数百万条消息。为了让事情更简单,我自己的实现使用尽可能少的汇编,使用单独的C函数来抽象复杂性。汇编代码将实现以下功能:idobjc_msgSend(idself,SEL_cmd,...){IMImmp=GetImplementation(self,_cmd);imp(self,_cmd,...);}GetImplementation可以以更具可读性的方式工作。汇编代码需要:1.将所有可能的参数存储在安全的地方,确保GetImplementation不会覆盖它们。2.调用GetImplementation。3.将返回值保存在某处。4.恢复所有参数值。5、跳转到GetImplementation返回的IMP。开始吧!在这里,我将尝试使用x86-64程序集,它可以轻松地在Mac上工作。这些概念也可以应用于i386或ARM。这个函数将保存在一个名为msgsend-asm.s的单独文件中。该文件可以像源文件一样传递给编译器,编译并链接到程序中。首先要做的是声明全局符号(globalsymbol)。由于一些无聊的历史原因,C函数的全局符号在其名称前有一个下划线:.globl_objc_msgSend_objc_msgSend:编译器将很乐意链接到最近的可用objc_msgSend。简单地将其链接到测试应用程序已经允许[objmessage]表达式使用我们自己的代码而不是Apple的运行时,这使得测试我们的代码以确保其工作变得相当方便。整数和指针参数被传递到寄存器%rsi、%rdi、%rdx、%rcx、%r8和%r9。其他类型的参数将被传递到堆栈(stack)上。这个函数做的最好的事情就是把这六个寄存器中的值保存在栈上,以便以后可以恢复:rax起着隐藏参数的作用。用于可变参数调用,保存传入的向量寄存器(vectorregisters)的个数,以便被调用函数正确准备可变参数列表。如果目标函数是可变参数方法,我也将值保存在这个寄存器中:pushq%rax为了完整性,用于传递浮点类型参数的寄存器%xmm也应该被保存。但是,如果我可以确保GetImplementation不会传入任何浮点数,我就可以忽略它们并且可以使代码更简洁。接下来,对齐堆栈。MacOSX要求函数调用堆栈按16字节边界对齐。上面的代码已经栈对齐了,但是还是需要手动显式处理,保证所有的东西都是对齐的,所以不用担心动态调用函数时会崩溃。为了对齐堆栈,我将%r12的原始值保存到堆栈后,将当前堆栈指针保存在%r12中。%r12是可选的,任何调用者保存的寄存器都可以。重要的是这些值在调用GetImplementation后仍然存在。然后我按位与(和)堆栈指针到-0x10,这清除了堆栈的底部四位:pushq%r12mov%rsp,%r12andq$-0x10,%rsp堆栈指针现在对齐了。这样可以安全的避开上面(上)保存的寄存器,因为栈是向下增长的,这种对齐方式会进一步向下移动。是时候调用GetImplementation了。它有两个参数,self和_cmd。调用约定是将这两个参数分别存放在%rsi和%rdi中。然而,当传递给objc_msgSend时,它们并没有被移动,所以没有必要改变它们。所做的一切其实就是调用了GetImplementation,方法名前还要加上下划线:callq_GetImplementation返回值对于整型和指针类型都存放在%rax中,返回的IMP就是在这里找到的。由于%rax需要恢复到原来的状态,返回的IMP需要移动到别处。我随机选择了%r11。mov%rax,%r11现在是恢复原状的时候了。先恢复%r12之前保存的栈指针,再恢复%r12的旧值:mov%r12,%rsppopq%r12然后按照入栈的相反顺序恢复寄存器的值:popq%raxpopq%r9popq%r8popq%rcxpopq%rdxpopq%rdipopq%rsi现在一切就绪。参数寄存器恢复到它们之前的状态。目标函数需要的参数都准备好了。IMP在寄存器%r11中,我们现在要做的就是跳转到那里:jmp*%r11就是这样!不需要其他汇编代码。jump将控制传递给方法实现。从代码来看,似乎是消息的发送者直接调用了这个方法。之前的迂回调用方式没有了。当方法返回时,它会直接放回到objc_msgSend的调用者,不需要进一步的操作。这个方法的返回值可以在合适的地方找到。非常规返回值有一些细微差别需要注意。比如大型结构体(无法存储在一个寄存器大小的返回值)。在x86-64上,大型结构返回时带有隐藏的第一个参数。当你这样调用时:NSRectr=SomeFunc(a,b,c);这个调用被翻译成这样:NSRectr;SomeFunc(&r,a,b,c);返回值的内存地址被传递到rdi中的%。由于objc_msgSend期望%rdi和%rsi包含self和_cmd,因此当消息返回大型结构时它不会工作。同样的问题存在于多个不同的平台上。runtime提供了objc_msgSend_stret返回结构体。工作原理和objc_msgSend类似,只是它知道在%rsi中找self,在%rdx中找_cmd。在某些平台上发送返回浮点值的消息(messages)时也会出现类似的问题。在这些平台上,运行时提供objc_msgSend_fpret(在x86-64上,objc_msgSend_fpret2用于特别极端的情况)。方法查找让我们继续实施GetImplementation。上面的汇编蹦床意味着这段代码可以用C实现。记住,在真正的运行时,这些代码是直接用汇编写的,以保证尽可能快的速度。这样不仅可以更好地控制代码,也可以避免重复上述保存和恢复寄存器的代码。GetImplementation可以简单地调用class_getMethodImplementation实现,混合在Objective-C运行时实现中。这有点无聊。真正的objc_msgSend首先查看类的方法缓存以最大化速度。由于GetImplementation想要模仿objc_msgSend,因此它也会这样做。如果缓存不包含给定的选择器入口点(entry),它将继续查找运行时(它回退到查询运行时)。我们现在需要的只是一些结构定义。方法缓存是类结构中的私有结构,要获取它,我们需要定义自己的版本。虽然是专有的,但这些结构的定义可以通过Apple的Objective-C运行时的开源实现获得(译注:http://opensource.apple.com/tarballs/objc4/)。首先你需要定义一个缓存条目:typedefstruct{SELname;void*unused;IMImp;}cache_entry;很简单。不要问我未使用的字段是做什么用的,我不知道它为什么在那里。这是缓存的完整定义:structobjc_cache{uintptr_tmask;uintptr_toccupied;cache_entry*buckets[1];};缓存是使用哈希表实现的。这个表是为了速度而实现的,其他的都被简化了,所以有点不同。表的大小总是2的幂,表以选择器为索引,桶直接以选择器的值为索引,可以通过移位去除不相关的低位,与进行逻辑与面具。这里有一些宏来计算给定选择器和掩码的桶的索引:#ifndef__LP64__#defineCACHE_HASH(sel,mask)(((uintptr_t)(sel)>>2)&(mask))#else#defineCACHE_HASH(sel,mask)(((unsignedint)((uintptr_t)(sel)>>0))&(mask))#endif***是类的结构。这是Class指向的类型:structclass_t{structclass_t*isa;structclass_t*superclass;structobjc_cache*cache;IMP*vtable;};所需的结构已经可用,现在让我们实现GetImplementation:IMPGetImplementation(idself,SEL_cmd){首先要做的是获取对象的类。真正的objc_msgSend是通过类似self->isa的方式获取的,但是会使用官方的API实现:Classc=object_getClass(self);因为我要访问最原始的形式,所以我会对指向class_t结构体的指针进行类型转换:structclass_t*classInternals=(structclass_t*)c;现在是时候找到IMP了。首先我们将它初始化为NULL。如果我们在缓存中找到它,我们就分配它。如果在查找缓存后仍然为NULL,我们将退回到较慢的方法:IMPimp=NULL;接下来,获取指向缓??存的指针:structobjc_cache*cache=classInternals->cache;计算bucket的索引,得到buckets数组指针:uintptr_tindex=CACHE_HASH(_cmd,cache->mask);cache_entry**buckets=cache->buckets;然后,我们使用我们要查找的选择器来查找缓存。运行时使用线性链接,然后遍历buckets子集,直到找到所需的条目或NULL条目:for(;buckets[index]!=NULL;index=(index+1)&cache->mask){if(buckets[index]->name==_cmd){imp=buckets[index]->imp;break;}}如果没有找到条目,我们调用运行时使用较慢的方法。在真正的objc_msgSend中,以上代码都是使用汇编实现的,是时候抛开汇编代码,调用runtime自己的方法了。一旦在缓存中查找不到想要的条目,快速发送消息的希望就会破灭。这个时候,获得更快的速度就没有那么重要了,因为注定会更慢,某种程度上,很少需要这样调用。因此,放弃汇编代码以支持更易于维护的C是可以接受的:if(imp==NULL)imp=class_getMethodImplementation(c,_cmd);无论如何,IMP现在已获取。如果它在缓存中,就会在那里找到它,否则由运行时查找。class_getMethodImplementation调用也会使用缓存,因此下一次调用会更快。剩下的就是返回IMP:returnimp;测试为了确保它有效,我写了一个快速测试程序:@interfaceTest:NSObject-(void)none;-(void)param:(int)x;-(void)params:(int)a:(int)b:(int)c:(int)d:(int)e:(int)f:(int)g;-(int)retval;@end@implementationTest-(id)init{fprintf(stderr,"ininitmethod,selfis%p\n",self);returnsself;}-(void)none{fprintf(stderr,"innonemethod\n");}-(void)param:(int)x{fprintf(stderr,"gotparameter%d\n",x);}-(void)params:(int)a:(int)b:(int)c:(int)d:(int)e:(int)f:(int)g{fprintf(stderr,"gotparams%d%d%d%d%d%d%d\n",a,b,c,d,e,f,g);}-(int)retval{fprintf(stderr,"inretvalmethod\n");return42;}@endintmain(intargc,char**argv){for(inti=0;i<20;i++){Test*t=[[Testalloc]init];[tnone];[tparam:9999];[tparams:1:2:3:4:5:6:7];fprintf(stderr,"retvalgaveus%d\n",[tretval]);NSMutableArray*a=[[NSMutableArrayalloc]init];[aaddObject:@1];[aaddObject:@{@"foo":@"bar"}];[aaddObject:@("blah")];a[0]=@2;NSLog(@"%@",A);}}为了防止对运行时实现进行一些意外调用,我向GetImplementation添加了一些调试日志以确保它被调用。一切正常,甚至文字和下标都在调用替换的实现。结论objc_msgSend的核心相当简单。但是它的实现需要一些汇编代码,这使得它比它应该的更难理解。但是为了性能优化,不得不使用一些汇编代码。但是通过构建一个简单的汇编蹦床,然后在C中实现它的逻辑,我们可以看到它是如何工作的,而且它真的没有什么高级之处。显然,您不应该在自己的应用程序中使用替代的objc_msgSend实现。你会后悔这样做的。这仅用于学习目的。
