iOS objc_msgSend尾调使用优化机制详解

作者 : 开心源码 本文共2273个字,预计阅读时间需要6分钟 发布时间: 2022-05-12 共165人阅读

级别:★★☆☆☆
标签:「objc_msgSend」「尾调使用优化」「尾递归」
作者: WYW、MrLiuQ
审校: QiShare团队

这篇文章的出现原因于Q·i Share团队对iOS 编写高质量Objective-C代码(二)中 (六、了解objc_msgSend(对象的消息传递机制))的激烈探讨。
注:Q·i Share是我们的团队名称,QiShare是我们的分享品牌。

这篇文章将认真彻底地分析 OC对objc_msgSend尾调使用优化。同时欢迎路过的大神留言探讨。

Q1:什么是尾调使用?

尾调使用(Tail Call):某个函数的最后一步仅仅只是调使用了一个函数(可以是自身,可以是另一个函数)。

QiShare提示:注意 “仅仅” 两个字。

尾调使用例子:
// 尾调使用:- (NSInteger)funcA:(NSInteger)num {    /*  Some codes... */    if (num == 0) {        return [self funcA:num];// 尾调使用->自身    }        if (num > 0) {        return [self funcB:num];// 尾调使用->函数funcB    }        return [self funcC:num];// 尾调使用->函数funcC}

正例解释:funcA的最后一步仅仅调使用了另一个函数。不管是调使用funcA、funcB还是funcC都属于尾调使用。(不管调使用函数的位置在哪,只需最后一步仅仅调使用一个函数就行)

反例:不是尾调使用的例子
// 不是尾调使用1:- (NSInteger)funcA:(NSInteger)num {    NSInteger num = [self funcB:(num)];    return num;// 不是尾调使用->最后一步是返回一个值,而不是调使用一个函数}

反例解释:不是尾调使用。由于最后一步是返回一个值,而不是仅仅调使用一个函数

// 不是尾调使用2:- (NSInteger)funcA:(NSInteger)num {    return [self funcB:(num)] + 1;// 不是尾调使用->起因:最后一步不仅调使用了函数还有 +1 操作}

反例解释:不是尾调使用。由于最后一步不仅调使用了函数还有 +1 操作


Q2:OC的尾调使用优化表现在哪里?

小编准备了一个demo:通过“断点”和“当前内存情况”查看有无尾调使用优化

场景一:无优化 – 追加了.0不属于尾调使用

无优化Demo效果图:无尾调使用优化

解释:
这种场景下,每次函数调使用一直在进栈,不断申请栈空间,最后会栈溢出,最终导致崩溃。
空间复杂度O(n),时间复杂度O(n)。

下面请看图解:


场景二:有尾调使用优化

优化Demo效果图:尾调使用优化

解释:
这种场景下,每次函数调使用一直在重使用栈帧,不申请栈空间。
空间复杂度O(1),时间复杂度O(n)。

下面请看图解:


Q3:OC是如何实现尾调使用优化的?

这次探讨原因于《Effective Objective-C 2.0》作者的原话:

假如某函数的最后一项操作是调使用另外一个函数,那么即可以运使用“ 尾调使用优化 ”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调使用堆栈中推入新的“栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调使用其余函数而不会将其返回值另作他使用时,才能执行“ 尾调使用优化 ”
这项优化对objc_msgSend非常关键,假如不这么做的话,那么每次调使用Objective-C方法之前,都需要为调使用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”(stack trace)中可以看到这种“栈帧”。此外,假如不优化,还会过早地发生“栈溢出”(stack overflow)现象。

作者这一段概括的话,很精简。而小编第一次看时,感觉很懵懂。在这里,QiShare对这段话进行了详细的分析:

  1. 尾调使用优化的本质:很简单,就是栈帧的复使用。

  2. 尾调使用优化的条件有三点:

    • 尾调使用函数不需要访问当前栈帧中的变量。(变量可以作为形参,但是不能作为实参)
    • 尾调使用返回后,函数没有语句需要执行。(最后一步仅仅只能执行一个函数)
    • 尾调使用结果就是函数的返回值。(不能有别的“附加品”,最后一步仅仅只能是执行一个函数)
  3. 函数调使用的过程:函数调使用会在内存中申请一块“栈帧”,保存调使用的地址和内部变量等信息。假如函数A内部调使用函数B,那么在函数A的栈帧上就会加上一个函数B的栈帧
    。假如函数B再调使用了函数C,那么函数A的栈帧上就会有序加上函数B和函数C的栈帧。假如C运行结束了,返回到函数B,C的栈帧才会消失。

4. 尾调使用优化实现原理:当函数A的最后一步仅仅是调使用另一个函数B时(或者者调使用自身函数A),这时,由于函数A的位置信息和内部变量已经不会再使用到了,直接把函数A的栈帧交给函数B用。

  1. 尾调使用优化关键图解:

总结:
1. 尾调使用:某个函数的最后一步仅仅调使用了一个函数(可以是自身,可以是另一个函数)。
2. OC的尾调使用优化的本质是:栈帧的复使用
3. 尾调使用优化实现原理:当函数A的最后一步仅仅是调使用另一个函数B时(或者者调使用自身函数A),这时,由于函数A的位置信息和内部变量已经不会再使用到了,直接把函数A的栈帧交给函数B用。

PS:尾调使用优化在Release模式下才会有,Debug模式下没有。

本文Demo源码地址

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS objc_msgSend尾调使用优化机制详解

发表回复