天天看點

iOS objc_msgSend尾調用優化詳解Q1:什麼是尾調用?Q2:OC的尾調用優化展現在哪裡?Q3:OC是如何實作尾調用優化的?

這篇文章将認真徹底地分析 OC對

objc_msgSend

尾調用優化

Q1:什麼是尾調用?

尾調用(

Tail Call

):某個函數的最後一步僅僅隻是調用了一個函數(可以是自身,可以是另一個函數)。

提醒:注意 “僅僅” 兩個字。

尾調用例子:
// 尾調用:
- (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的尾調用優化的本質是:[棧幀](https://baike.so.com/doc/9968763-10316382.html)的複用
3. 尾調用優化實作**原理**:當函數A的最後一步僅僅是調用另一個函數B時(或者調用自身函數A),這時,因為函數A的位置資訊和内部變量已經不會再用到了,直接把函數A的棧幀交給函數B使用。
           

PS:尾調用優化在Release模式下才會有,Debug模式下沒有。

本文Demo源碼位址

繼續閱讀