iOS - 老生常談內存管理(一):引用計數

簡單聊聊 GC 與 RC

隨着各個平臺的發展,如今被普遍採用的內存管理機制主要有 GC 和 RC 兩種。html

  • GC (Garbage Collection):垃圾回收機制,按期查找再也不使用的對象,釋放對象佔用的內存。
  • RC (Reference Counting):引用計數機制。採用引用計數來管理對象的內存,當須要持有一個對象時,使它的引用計數 +1;當不須要持有一個對象的時候,使它的引用計數 -1;當一個對象的引用計數爲 0,該對象就會被銷燬。

Objective-C支持三種內存管理機制:ARCMRCGC,但Objective-CGC機制有平臺侷限性,僅限於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安全

Reference Counting

做爲一名 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指針找到對應的方法、實例變量、屬性、協議等;
  • 在 arm64 架構以前,isa就是一個普通的指針,直接指向objc_class,存儲着ClassMeta-Class對象的內存地址。instance對象的isa指向class對象,class對象的isa指向meta-class對象;
  • 從 arm64 架構開始,對isa進行了優化,用nonpointer表示,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。將 64 位的內存數據分開來存儲着不少的東西,其中的 33 位纔是拿來存儲classmeta-class對象的內存地址信息。要經過位運算將isa的值& ISA_MASK掩碼,才能獲得classmeta-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__ 架構下 }; 複製代碼

若是isanonpointer,即 arm64 架構以前的isa指針。因爲它只是一個普通的指針,存儲着ClassMeta-Class對象的內存地址,因此它自己不能存儲引用計數,因此之前對象的引用計數都存儲在一個叫SideTable結構體的RefCountMap(引用計數表)散列表中。

若是isanonpointer,則它自己能夠存儲一些引用計數。從以上union isa_t的定義中咱們能夠得知,isa_t中存儲了兩個引用計數相關的東西:extra_rchas_sidetable_rc

  • extra_rc:裏面存儲的值是對象自己以外的引用計數的數量,這 19 位若是不夠存儲,has_sidetable_rc的值就會變爲 1;
  • has_sidetable_rc:若是爲 1,表明引用計數過大沒法存儲在isa中,那麼超出的引用計數會存儲SideTableRefCountMap中。

因此,若是isanonpointer,則對象的引用計數存儲在它的isa_textra_rc中以及SideTableRefCountMap中。

備註

  • 以上isa_t結構來自老版本的objc4源碼,從objc4-750版本開始,isa_t中的struct的內容定義成了宏並寫在isa.h文件裏,不過其數據結構不變,這裏不影響。
  • 更多關於isa的知識,以及以上提到的一些細節,能夠查看《深刻淺出 Runtime(二):數據結構》

SideTable

以上提到了一個數據結構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 - 老生常談內存管理(四):源碼分析內存管理方法》

相關文章
相關標籤/搜索