Swift高階 - 內存管理:ARC, Strong, Weak and Unowned詳解

image

內存管理是任何編程語言中的核心概念。 儘管有不少教程解釋了Swift自動引用計數的基本原理,但我發現沒有一個能夠從編譯器的角度對其進行解釋。 在本文中,咱們將學習iOS內存管理,引用計數和對象生命週期等基礎知識以外的內容。git

讓咱們從基礎開始,逐步進入ARC和Swift Runtime的內部,首先思考如下問題:程序員

  • 內存是什麼?
  • Swift編譯器是如何實現自動引用計數的?
  • 強,弱和無主引用是如何實現的?
  • Swift對象的生命週期是怎麼樣的?
  • 什麼是side table?

內存管理

從硬件層面,內存只是一長串字節。 在虛擬內存中它被分紅三個主要部分:github

  • 棧區,全部局部變量都存放在哪裏。
  • 全局數據,其中包含靜態變量,常量和類型元數據。
  • 堆區,全部動態分配的對象都在其中。 基本上,全部具備生命週期的東西都存儲在這裏。

咱們將繼續交替使用「對象」和「動態分配的對象」。 這些是Swift引用類型以及值類型的一些特殊狀況。編程

內存管理是控制程序內存的過程。 瞭解它的工做原理相當重要,不然您可能會遇到隨機崩潰和莫名的小bug。swift

ARC

內存管理與全部權的概念緊密相關。 全部權會決定哪些代碼會形成對象被銷燬[1]。安全

自動引用計數(ARC)屬於Swift的全部權系統,它規定了一組用於管理和轉讓全部權的約定。bash

能夠指向對象的變量別名叫作引用。 Swift引用具備兩個強度級別:強和弱。 此外,弱引用包含無主引用和弱引用。app

Swift內存管理的本質是:若是一個對象被強引用指向,Swift會保留它,不然將其釋放。 剩下的只是實現細節。編程語言

理解Strong, Weak and Unowned

強引用的目的是使對象保持存活狀態。 強引用可能會致使幾個有意義的問題[2]:ide

  • 循環引用。 考慮到Swift語言不是循環收集(cycle-collecting)的,一個對象的強引用R若是同時被對象強引用(多是間接的),則會致使循環引用。 咱們必須編寫大量代碼來顯式打破循環。
  • 並不是老是可使強引用在對象構造上當即有效,例如代理(delegates)。

弱引用解決了反向引用的問題。 若是有指向對象的弱引用,則能夠銷燬該對象。 弱引用訪問再也不存在的對象時將返回nil。 這稱爲調零或歸零(zeroing)。

無主引用是弱函數的另外一種形式,旨在用於嚴格的有效性不變式。 無主引用是非歸零的。 當試圖經過無主引用讀取不存在的對象時,程序將因斷言錯誤而崩潰。 它們用於跟蹤和修復一致性問題頗有用。

class MyClass {	
    lazy var foo = { [weak self] in	
        // Must be validated	
        guard let self = self else { return }	
        self.doSomething()	
    }()	
    func doSomething() {}	
}
複製代碼

無主引用無需在使用時進行驗證:

lazy var bar = { [unowned self] in	
  // No validation needed
  self.doSomething()	
}()	
複製代碼

在這個示例中,使用無主引用是明智的,由於屬性barself具備相同的生存期。

咱們對Swift內存管理的進一步討論會處於較低的抽象層面。 咱們將深刻研究如何在編譯器級別實現ARC,以及每一個Swift對象在銷燬以前要經歷的步驟。

Swift Runtime

ARC機制在Swift Runtime庫中聲明。 它包含了諸如運行時類型系統之類的核心功能,例如:動態轉換,泛型和協議一致性註冊[3]

Swift Runtime 使用HeapObject結構體表示每一個動態分配的對象。 它包含構成Swift對象的全部數據:引用計數和類型元數據。

HeapObject中每一個Swift對象都有三個引用計數:每種引用都有一個。 在SIL生成階段,swiftc編譯器會在適當的地方插入swift_retain()swift_release()函數。 這是經過攔截HeapObject的初始化和銷燬來完成的。

編譯是Xcode Build System的步驟之一

若是您是Objective-C老程序員,而且想知道autorelease在哪裏,能夠告訴你:純Swift對象沒有這個東西。

如今,讓咱們繼續弱引用。 它們的實現方式與Side table的概念緊密相關。

想要詳細瞭解SideTable,請閱讀我以前的一篇文章:Swift弱引用管理之Side Table

Side Tables介紹

Side tables 是實現Swift弱引用的核心。

大多數狀況,對象沒有任何「弱」引用,所以爲每一個對象中的弱引用計數保留存儲空間是浪費的。 此信息存儲在外部的 side table中,只有在確實須要時纔會分配。

弱引用變量不是直接指向對象,而是指向side table,而side table又指向對象。 這解決了兩個問題:爲弱引用計數節省內存,直到對象真正須要它才建立; 容許安全地將弱引用歸零,由於它不會直接指向對象,而且再也不是竟態條件的主體。

當兩個線程競爭同一資源時,若是對資源的訪問順序敏感,就稱存在競態條件。

Side table只包含一個引用計數 和 一個對象的指針。 它們在Swift Runtime 中聲明以下(C ++ 代碼)[5]:

class HeapObjectSideTableEntry {
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
  // Operations to increment and decrement reference counts
}
複製代碼

Swift對象生命週期

Swift對象具備本身的生命週期,在下圖中我用有限狀態機表示。 方括號表示觸發狀態轉換的條件。

1

Live狀態時,對象處於活動狀態。 其引用計數被初始化爲 strong:1, unown:1和 weak:1(side table從+1開始)。 一旦有弱引用指向對象,便會建立side table。 弱引用指向side table而不是對象。

一旦強引用計數達到零,則對象從Live狀態進入Deiniting狀態。 處於Deiniting狀態表示deinit()正在進行中。 在這一點上,強引用操做無效。 若是存在關聯的side table,經過弱引用訪問將返回nil。 經過unowned訪問將觸發斷言失敗。 經過新的unowned引用仍然能夠存儲。 今後狀態開始,可能選擇兩條分支:

  • 快速判斷若是沒有weak,unowned的引用和side table。 該對象將轉換爲Dead狀態,並當即從內存中刪除。
  • 不然,對象將變爲Deinited狀態。

Deinited狀態下,deinit()已經執行完成,該對象還有未完成的unown引用(至少是初始值:1)。 此時,經過強和弱引用進行存儲和讀取沒法發生。 Unowned引用存儲也不會發生。 經過Unown讀取會觸發斷言錯誤。 該對象能夠今後處進入兩條分支:

  • 若是沒有弱引用,則能夠當即釋放該對象。 它過渡到Dead狀態。
  • 不然,仍然有一個side table要移除,而且對象進入Freed狀態。

Freed狀態以前,對象已徹底釋放,但它的 side table仍處於活動狀態。 在此階段,弱引用計數將置0,而且 side table會被銷燬。 對象將轉換爲最終狀態。

除指向對象的指針外,在Dead狀態下對象已被所有銷燬。 指向「HeapObject」的指針也從堆中釋放出來,在內存中找不到該對象的任何痕跡。

總結

自動引用計數並非什麼神奇的東西,咱們對它越瞭解,咱們的代碼就越不容易出現內存管理錯誤。 這裏是要記住的幾個關鍵點:

  • 弱引用指針指向side table。 無主和強引用指針指向對象。
  • 自動引用計數是在編譯器級別實現的。 swiftc編譯器會在適當的時候插入swift_retain()swift_release()
  • Swift對象不會當即銷燬。 它們在生命週期中經歷了五個階段:live -> deiniting -> deinited -> freed -> dead

做者:Vadim Bulavin 翻譯:樂Coding

推薦

[1] : github.com/apple/swift…

[2] : github.com/apple/swift…

[3] : github.com/apple/swift…

[4] : github.com/apple/swift…

[HeapObject] : (github.com/apple/swift…

[6] : github.com/apple/swift…


logo
相關文章
相關標籤/搜索