本文基于Objective-C对象的消息传递机制,详细分析了OC对objc_msgSend的尾调用优化方法。1.什么是尾调用?尾调用(TailCall):函数的最后一步只是调用了一个函数(可以是它自己,也可以是另一个函数)。奇享提醒:注意“只”字。尾调用示例://Tailcall:-(NSInteger)funcA:(NSInteger)num{/*Somecodes...*/if(num==0){return[selffuncA:num];//Tailcall->self}if(num>0){return[selffuncB:num];//Tailcall->functionfuncB}return[selffuncC:num];//Tailcall->functionfuncC}正例讲解:funcA的最后一步即可另一个函数被调用。无论是调用funcA、funcB还是funcC,都是尾调用。(不管函数在哪里调用,只要最后一步只调用了一个函数即可){NSIntegernum=[selffuncB:(num)];returnnum;//不是尾调用->最后一步是返回值而不是调用函数}反例解释:不是尾调用。因为最后一步是返回一个值,而不是仅仅调用一个函数。//不是尾调用2:-(NSInteger)funcA:(NSInteger)num{return[selffuncB:(num)]+1;//不是尾调用->原因:末尾有+1操作}否定解释:不是尾调用。因为最后一步不仅调用了函数,还调用了+1操作。2、OC的尾调用优化体现在哪里?小编准备了一个demo:通过“断点”和“当前内存情况”查看是否有尾调用优化。场景一:没有优化(因为加了.0,所以不是尾调用)没有优化的demo渲染:这个场景下,每次函数调用都不停的入栈,不断的申请栈空间,最终栈溢出,最终导致崩溃。空间复杂度O(n),时间复杂度O(n)。示意图如下:场景二:尾调用优化优化Demo效果图:该场景下,每次函数调用一直重复使用栈帧,不申请栈空间。空间复杂度O(1),时间复杂度O(n)。示意图如下:3、OC是如何实现尾调用优化的?本讨论来源于《Effective Objective-C 2.0》的原文:如果一个函数的最后一个操作是调用另一个函数,那么可以使用“尾调用优化”技术。编译器生成切换到另一个函数所需的脚本,而无需将新的“帧堆栈”推入调用堆栈。“尾调用优化”只能在一个函数的最后一个操作是调用其他函数而不将其返回值用于其他目的时进行。这个优化对于objc_msgSend来说是非常关键的。如果不这样做,则需要在每次调用Objective-C方法之前为调用objc_msgSend函数准备一个“堆栈框架”。你可以在“堆栈跟踪”中看到这个“堆栈框架”。另外,如果不优化,会过早出现“堆栈溢出”(stackoverflow)。作者对tailcalls的描述非常简洁。这里,QiShare团队对这段话进行了详细的分析:(1)尾调用优化的本质:很简单,就是栈帧的复用。(2)尾调用优化的三个条件:尾调用函数不需要访问当前栈帧中的变量。(变量可以作为形参,不能作为实参)尾调用返回后,函数没有语句可执行。(最后一步只能执行一个函数)尾调用的结果就是函数的返回值。(不能有其他的“附加品”,最后一步只执行一个函数)(3)函数调用过程:函数调用会在内存中申请一个“栈帧”,保存调用地址和内部变量和其他信息。如果函数A在内部调用函数B,则函数B的栈帧会被添加到函数A的栈帧中。如果函数B再次调用函数C,函数B和函数C的栈帧会被添加到函数A的栈帧中功能A的顺序。如果C运行完毕,返回函数B,C的栈帧就会消失。(4)尾调用优化的实现原理:当函数A的最后一步要调用另一个函数B(或者调用自己的函数A)时,此时,因为不再使用函数A的位置信息和内部变量到了,直接把函数A的栈帧交给函数B使用。尾调用优化关键图解:总结:尾调用:一个函数的最后一步只调用一个函数(可以是它自己,也可以是另一个函数)。OC的尾调用优化的本质是:栈帧的复用尾调用优化实现原理:当函数A的最后一步只是调用另一个函数B(或者调用自己的函数A)时,此时,由于位置信息而内部变量将不再使用,函数A的栈帧直接交给函数B使用。PS:尾调用优化只在Release模式下有效,在Debug模式下无效。源码地址:https://github.com/QiShare/QiRecursiveDemo.git【本文为360技术专栏原创文章,微信公众号《360技术(id:qihoo_tech)》】点击这里查看作者的更多好文章
