iOS objc_msgSend尾調用優化機制詳解

級別:★★☆☆☆
標籤:「objc_msgSend」「尾調用優化」「尾遞歸」
做者: WYWMrLiuQ
審校: QiShare團隊php

這篇文章的出現原由於QiShare團隊對iOS 編寫高質量Objective-C代碼(二)中 (6、理解objc_msgSend(對象的消息傳遞機制))的激烈討論。
html

這篇文章將認真完全地分析 OC對objc_msgSend的「尾調用優化」。同時歡迎路過的大神留言討論。
git

Q1:什麼是尾調用?

尾調用(Tail Call):某個函數的最後一步僅僅只是調用了一個函數(能夠是自身,能夠是另外一個函數)。

QiShare提醒:注意 「僅僅」 兩個字。github

尾調用例子:
// 尾調用:
- (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:經過「斷點」和「當前內存狀況」查看有無尾調用優化spa

場景一:無優化 - 追加了0.0不屬於尾調用

無優化Demo效果圖:3d

無尾調用優化

解釋: 這種場景下,每次函數調用一直在進棧,不斷申請棧空間,最後會棧溢出,最終致使崩潰。
空間複雜度O(n),時間複雜度O(n)。
code

下面請看圖解:


場景二:有尾調用優化

優化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(微信公衆號)

相關文章
相關標籤/搜索