隨着各個平臺的發展,如今被普遍採用的內存管理機制主要有 GC 和 RC 兩種。html
Objective-C
支持三種內存管理機制:ARC
、MRC
和GC
,但Objective-C
的GC
機制有平臺侷限性,僅限於MacOS
開發中,iOS
開發用的是RC
機制,從MRC
到如今的ARC
。編程
備註: 蘋果在引入
ARC
的時候稱將在MacOS
中棄用GC
機制。
OS X Mountain Lion v10.8 中不推薦使用GC
機制,而且將在 OS X 的將來版本中刪除GC
機制。ARC
是推薦的替代技術。爲了幫助現有應用程序遷移,Xcode 4.3 及更高版本中的ARC
遷移工具支持將使用GC
的 OS X 應用程序遷移到ARC
。
注意:對於面向 Mac App Store 的應用,Apple 強烈建議你儘快使用ARC
替換GC
,由於 Mac App Store Guidelines 禁止使用已棄用的技術,不然不會經過審覈,詳情請參閱 Mac App Store Review Guidelines。安全
做爲一名 iOS 開發者,引用計數機制是咱們必須掌握的知識。那麼,引用計數機制下是怎樣工做的呢?它存在什麼優點?數據結構
在《Objective-C 高級編程:iOS 與 OS X 多線程和內存管理》這本書中舉了一個 「辦公室裏的照明問題」 的例子,很好地說明了引用計數機制。多線程
假設辦公室裏的照明設備只有一個。上班進入辦公室的人須要照明,因此要把燈打開。而對於下班離開辦公室的人來講,已經不須要照明瞭,因此要把燈關掉。架構
如果不少人上下班,每一個人都開燈或者關燈,那麼辦公室的狀況又將如何呢?最先下班的人若是關了燈,那就會像下圖那樣,辦公室裏還沒走的全部人都將處於一片黑暗之中。併發
解決這一問題的辦法就是使辦公室在還有至少一人的狀況下保持開燈狀態,而在無人時保持關燈狀態。app
(1)最先進入辦公室的人開燈。
(2)以後進入辦公室的人,須要照明。
(3)下班離開辦公室的人,不須要照明。
(4)最後離開辦公室的人關燈(此時已無人須要照明)。ide
爲判斷是否還有人在辦公室裏,這裏導入計數功能來計算 「須要照明的人數」。下面讓咱們來看看這一功能是如何運做的吧。函數
(1)第一我的進入辦公室,「須要照明的人數」 加 1。計數值從 0 變成了 1,所以要開燈。
(2)以後每當有人進入辦公室,「須要照明的人數」 就加 1。如計數值從 1 變成 2。
(3)每當有人下班離開辦公室,「須要照明的人數」 就減 1。如計數值從 2 變成 1。
(4)最後一我的下班離開辦公室,「須要照明的人數」 減 1。計數值從 1 變成了 0,所以要關燈。
這樣就能在不須要照明的時候保持關燈狀態。辦公室中僅有的照明設備獲得了很好的管理,以下圖所示:
在 Objective-C 中,「對象」 至關於辦公室裏的照明設備。在現實世界中辦公室裏的照明設備只有一個,但在 Objective-C 的世界裏,雖然計算機的資源有限,但一臺計算機能夠同時處理好幾個對象。
此外,「對象的使用環境」 至關於上班進入辦公室的人。雖然這裏的 「環境」 有時也指在運行中的程序代碼、變量、變量做用域、對象等,但在概念上就是使用對象的環境。上班進入辦公室的人對辦公室照明設備發出的動做,與 Objective-C 中的對應關係則以下表所示:
對照明設備所作的動做 | 對 Objective-C 對象所作的動做 |
---|---|
開燈 | 生成對象 |
須要照明 | 持有對象 |
不須要照明 | 釋放對象 |
關燈 | 廢棄對象 |
使用計數功能計算須要照明的人數,使辦公室的照明獲得了很好的管理。一樣,使用引用計數功能,對象也就可以獲得很好的管理,這就是 Objective-C 的內存管理。以下圖所示:
以上咱們對 「引用計數」 這一律唸作了初步瞭解,Objective-C 中的 「對象」 經過引用計數功能來管理它的內存生命週期。那麼,對象的引用計數是如何存儲的呢?它存儲在哪一個數據結構裏?
首先,不得不提一下isa
。
isa
指針用來維護 「對象」 和 「類」 之間的關係,並確保對象和類可以經過isa
指針找到對應的方法、實例變量、屬性、協議等;isa
就是一個普通的指針,直接指向objc_class
,存儲着Class
、Meta-Class
對象的內存地址。instance
對象的isa
指向class
對象,class
對象的isa
指向meta-class
對象;isa
進行了優化,用nonpointer
表示,變成了一個共用體(union
)結構,還使用位域來存儲更多的信息。將 64 位的內存數據分開來存儲着不少的東西,其中的 33 位纔是拿來存儲class
、meta-class
對象的內存地址信息。要經過位運算將isa
的值& ISA_MASK
掩碼,才能獲得class
、meta-class
對象的內存地址。// objc.h
struct objc_object {
Class isa; // 在 arm64 架構以前
};
// objc-private.h
struct objc_object {
private:
isa_t isa; // 在 arm64 架構開始
};
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// nonpointer must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__ // 在 __arm64__ 架構下
# define ISA_MASK 0x0000000ffffffff8ULL // 用來取出 Class、Meta-Class 對象的內存地址
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 0:表明普通的指針,存儲着 Class、Meta-Class 對象的內存地址
// 1:表明優化過,使用位域存儲更多的信息
uintptr_t has_assoc : 1; // 是否有設置過關聯對象,若是沒有,釋放時會更快
uintptr_t has_cxx_dtor : 1; // 是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快
uintptr_t shiftcls : 33; // 存儲着 Class、Meta-Class 對象的內存地址信息
uintptr_t magic : 6; // 用於在調試時分辨對象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否有被弱引用指向過,若是沒有,釋放時會更快
uintptr_t deallocating : 1; // 對象是否正在釋放
uintptr_t has_sidetable_rc : 1; // 若是爲1,表明引用計數過大沒法存儲在 isa 中,那麼超出的引用計數會存儲在一個叫 SideTable 結構體的 RefCountMap(引用計數表)散列表中
uintptr_t extra_rc : 19; // 裏面存儲的值是對象自己以外的引用計數的數量,retainCount - 1
# define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; ...... // 在 __x86_64__ 架構下 }; 複製代碼
若是isa
非nonpointer
,即 arm64 架構以前的isa
指針。因爲它只是一個普通的指針,存儲着Class
、Meta-Class
對象的內存地址,因此它自己不能存儲引用計數,因此之前對象的引用計數都存儲在一個叫SideTable
結構體的RefCountMap
(引用計數表)散列表中。
若是isa
是nonpointer
,則它自己能夠存儲一些引用計數。從以上union isa_t
的定義中咱們能夠得知,isa_t
中存儲了兩個引用計數相關的東西:extra_rc
和has_sidetable_rc
。
has_sidetable_rc
的值就會變爲 1;isa
中,那麼超出的引用計數會存儲SideTable
的RefCountMap
中。因此,若是isa
是nonpointer
,則對象的引用計數存儲在它的isa_t
的extra_rc
中以及SideTable
的RefCountMap
中。
備註:
- 以上
isa_t
結構來自老版本的objc4
源碼,從objc4-750
版本開始,isa_t
中的struct
的內容定義成了宏並寫在isa.h
文件裏,不過其數據結構不變,這裏不影響。- 更多關於
isa
的知識,以及以上提到的一些細節,能夠查看《深刻淺出 Runtime(二):數據結構》。
以上提到了一個數據結構SideTable
,咱們進入objc4
源碼查看它的定義。
// NSObject.mm
struct SideTable {
spinlock_t slock; // 自旋鎖
RefcountMap refcnts; // 引用計數表(散列表)
weak_table_t weak_table; // 弱引用表(散列表)
......
}
複製代碼
SideTable
存儲在SideTables()
中,SideTables()
本質也是一個散列表,能夠經過對象指針來獲取它對應的(引用計數表或者弱引用表)在哪個SideTable
中。在非嵌入式系統下,SideTables()
中有 64 個SideTable
。如下是SideTables()
的定義:
// NSObject.mm
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
複製代碼
因此,查找對象的引用計數表須要通過兩次哈希查找:
SideTables()
中取出它所在的SideTable
;SideTable
中的refcnts
中取出它的引用計數表。Q:爲何不是一個
SideTable
,而是使用多個SideTable
組成SideTables()
結構?
若是隻有一個SideTable
,那咱們在內存中分配的全部對象的引用計數或者弱引用都放在這個SideTable
中,那咱們對對象的引用計數進行操做時,爲了多線程安全就要加鎖,就存在效率問題。
系統爲了解決這個問題,就引入 「分離鎖」 技術方案,提升訪問效率。把對象的引用計數表分拆多個部分,對每一個部分分別加鎖,那麼當所屬不一樣部分的對象進行引用操做的時候,在多線程下就能夠併發操做。因此,使用多個SideTable
組成SideTables()
結構。
備註: 關於引用計數具體是怎麼管理的,請參閱
《iOS - 老生常談內存管理(四):源碼分析內存管理方法》
。