Objective-C 拾遺:從Heap and Stack到Block

發表於2015-04-28

Stack和Heap

heap和stack是記憶體管理的兩個重要概念。在這裡我們指的不是資料結構上面的堆與棧,在這裡指的是記憶體的分配區域。

  1. stack的空間由作業系統進行分配。

    在現代作業系統中,一個執行緒會分配一個stack. 當一個函式被呼叫,一個stack frame(棧幀)就會被壓到stack裡。裡面包含這個函式涉及的引數,區域性變數,返回地址等相關資訊。當函式返回後,這個棧幀就會被銷燬。而這一切都是自動的,由系統幫我們進行分配與銷燬。對於程式設計師是透明的,我們不需要手動排程。

  2. heap的空間需要手動分配。

    heap與動態記憶體分配相關,記憶體可以隨時在堆中分配和銷燬。我們需要明確請求記憶體分配與記憶體銷燬。 簡單來說,就是malloc與free.

Objective-C中的Stack和Heap

首先所有的Objective-C物件都是分配在heap的。 在OC最典型的記憶體分配與初始化就是這樣的。

一個物件在alloc的時候,就在Heap分配了記憶體空間。

stack物件通常有速度的優勢,而且不會發生記憶體洩露問題。那麼為什麼OC的物件都是分配在heap的呢? 原因在於:

  1. stack物件的生命週期所導致的問題。例如一旦函式返回,則所在的stack frame就會被摧毀。那麼此時返回的物件也會一併摧毀。這個時候我們去retain這個物件是無效的。因為整個stack frame都已經被摧毀了。簡單而言,就是stack物件的生命週期不適合Objective-C的引用計數記憶體管理方法。
  2. stack物件不夠靈活,不具備足夠的擴充套件性。建立時長度已經是固定的,而stack物件的擁有者也就是所在的stack frame

關於Block

問題的由來:

洋洋灑灑講了前面這些東西,其實為什麼決定寫這篇總結呢,簡單講講最初的問題。

那就是在之前我在類中宣告block屬性時,一直用的是strong修飾符。因為我一直把block當成一個普通的OC物件來看待。並且也沒有出現過問題。後來閱讀一些別人的原始碼和部落格,發現不少人都是使用copy修飾符,於是引起了這篇探索。可以簡單地把這個問題總結為:為什麼block需要使用copy修飾符?

簡單的答案:

首先在官方文件《Programming with Objective-C》裡面寫到,初學閱讀的時候沒有注意到這個細節:

You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn’t something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it’s best practice for the property attribute to show the resultant behavior

在這裡官方叫我們使用copy修飾符,雖然在ARC時代已經不需要再顯式宣告瞭,也就是使用strong是沒有問題的,但是仍然建議我們使用copy以顯示相關拷貝行為。問題到這裡就基本結束了。目前使用strong和copy都是沒有問題的。

深入探索:

但是在這裡仍然無法解答我的疑惑,需要使用copy修飾符的根本原因是什麼。所以繼續探索。

最終得到的答案是這與block物件在建立時是stack物件有關。
所以,其實Objective-C是有它的Stack object的。是的,那就是block.

首先我們先對block進行進一步的認識。

在Objective-C語言中,一共有3種型別的block:

_NSConcreteGlobalBlock 全域性的靜態block,不會訪問任何外部變數。

_NSConcreteStackBlock 儲存在棧中的block,當函式返回時會被銷燬。

_NSConcreteMallocBlock 儲存在堆中的block,當引用計數為0時會被銷燬。

這裡我們主要基於記憶體管理的角度對它們進行分類。

  • NSConcreteGlobalBlock,這種不捕捉外界變數的block是不需要記憶體管理的,這種block不存在於Heap或是Stack而是作為程式碼片段存在,類似於C函式。
  • NSConcreteStackBlock。這就是這次探索的重點了,需要涉及到外界變數的block在建立的時候是在stack上面分配空間的,也就是一旦所在函式返回,則會被摧毀。這就導致記憶體管理的問題,如果我們希望儲存這個block或者是返回它,如果沒有做進一步的copy處理,則必然會出現問題。

舉個例子(來自參考資料block quiz),在手動管理引用計數時,如果在exampleD_getBlock方法返回block時對block進行[[block copy] autorelease]的操作,則方法執行完畢後,block就會被銷燬,則返回block是無效的。

  • NSConcreteMallocBlock,因此為了解決block作為Stack object的這個問題,我們最終需要把它拷貝到堆上面來。而此時NSConcreteMallocBlock扮演的就是這個角色。

    拷貝到堆後,block的生命週期就與一般的OC物件一樣了,我們通過引用計數來對其進行記憶體管理。

真正的答案:

因此答案便是因為block在建立時是stack物件,如果我們需要在離開當前函式仍能夠使用我們建立的block。我們就需要把它拷貝到堆上以便進行以引用計數為基礎的記憶體管理。

ARC的疑團:

解答完最初的問題後,新的問題又出現在我的腦海。那就是ARC是如何進行block的記憶體管理的呢,對於普通的OC物件之前已經在記憶體管理裡面進行總結過。

那麼block在ARC下是如何從棧管理正確過渡到堆的管理的呢:

我在網上查閱了許多資料與博文,有部分總結是:

在ARC下NSConcreteStackBlock型別的block會替換成NSConcreteMallocBlock型別

其實這是不夠準確的,來自蘋果LLVM ARC的文件中談到:

With the exception of retains done as part of initializing a __strong parameter variable or reading a __weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

也就是說ARC幫助我們完成了copy的工作,在ARC下,即使你宣告的修飾符是strong,實際上效果是與宣告為copy一樣的。

因此在ARC情況下,建立的block仍然是NSConcreteStackBlock型別,只不過當block被引用或返回時,ARC幫助我們完成了copy和記憶體管理的工作。

總結和心得:

其實用一句話總結便是:
在ARC下,我們可以將block看做一個正常的OC物件,與其他物件的記憶體管理沒什麼不同。

有時我們可能簡單地從部落格和文件上面得到一句簡單的結論就夠了。但是如果我們不斷探索,不斷思考,那麼我們的收穫會更大,更深。可能不僅僅是一句知識點,更多的是探索的方法和過程。對一件事情剝繭抽絲,還原本質的過程對我來說也是一種享受,一種修行。

經過了一系列探索,最終理解了block的概念,瞭解了block的實現,弄懂了block的記憶體管理。
加油,繼續修行~

參考資料:

What and where are the stack and heap?

Cocoa blocks as strong pointers vs copy

Should I still copy/Block_copy the blocks under ARC?

Stack and Heap Objects in Objective-C

談Objective-C Block的實現

Objective-C Blocks Quiz

相關文章