Swift 記憶體管理,WEAK 和 UNOWNED

2021-07-11 06:46:03 字數 3396 閱讀 4885

不管在什麼語言裡,記憶體管理的內容都很重要,所以我打算花上比其他 tip 長一些的篇幅仔細地說說這塊內容。

swift 是自動管理記憶體的,這也就是說,我們不再需要操心記憶體的申請和分配。當我們通過初始化建立乙個物件時,swift 會替我們管理和分配記憶體。而釋放的原則遵循了自動引用計數 (arc) 的規則:當乙個物件沒有引用的時候,其記憶體將會被自動**。這套機制從很大程度上簡化了我們的編碼,我們只需要保證在合適的時候將引用置空 (比如超過作用域,或者手動設為 nil 等),就可以確保記憶體使用不出現問題。

但是,所有的自動引用計數機制都有乙個從理論上無法繞過的限制,那就是迴圈引用 (retain cycle) 的情況。

什麼是迴圈引用

雖然我覺得迴圈引用這樣的概念介紹不太應該出現在這本書中,但是為了更清晰地解釋 swift 中的迴圈引用的一般情況,這裡還是簡單進行說明。假設我們有兩個類 a 和 b, 它們之中分別有乙個儲存屬性持有對方:

class

a deinit

}class

b }

在 a 的初始化方法中,我們生成了乙個 b 的例項並將其儲存在屬性中。然後我們又將 a 的例項賦值給了 b.a。這樣 a.b 和 b.a 將在初始化的時候形成乙個引用迴圈。現在當有第三方的呼叫初始化了 a,然後即使立即將其釋放,a 和 b 兩個類例項的 deinit 方法也不會被呼叫,說明它們並沒有被釋放。

var obj: a? = a()

obj = nil

// 記憶體沒有釋放

因為即使 obj 不再持有 a 的這個物件,b 中的 b.a 依然引用著這個物件,導致它無法釋放。而進一步,a 中也持有著 b,導致 b 也無法釋放。在將 obj 設為 nil 之後,我們在**裡再也拿不到對於這個物件的引用了,所以除非是殺掉整個程序,我們已經永遠也無法將它釋放了。多麼悲傷的故事啊..

在 swift 裡防止迴圈引用

為了防止這種人神共憤的悲劇的發生,我們必須給編譯器一點提示,表明我們不希望它們互相持有。一般來說我們習慣希望 「被動」 的一方不要去持有 「主動」 的一方。在這裡 b.a 裡對 a 的例項的持有是由 a 的方法設定的,我們在之後直接使用的也是 a 的例項,因此認為 b 是被動的一方。可以將上面的 class b 的宣告改為:

class

b }

在 var a 前面加上了 weak,向編譯器說明我們不希望持有 a。這時,當 obj 指向 nil 時,整個環境中就沒有對 a 的這個例項的持有了,於是這個例項可以得到釋放。接著,這個被釋放的例項上對 b 的引用 a.b 也隨著這次釋放結束了作用域,所以 b 的引用也將歸零,得到釋放。新增 weak 後的輸出:

a deinit

b deinit

我們結合實際編碼中的使用來看看選擇吧。日常工作中一般使用弱引用的最常見的場景有兩個:

設定 delegate 時

在 self 屬性儲存為閉包時,其中擁有對 self 引用時

前者是 cocoa 框架的常見設計模式,比如我們有乙個負責網路請求的類,它實現了傳送請求以及接收請求結果的任務,其中這個結果是通過實現請求類的 protocol 的方式來實現的,這種時候我們一般設定 delegate 為 weak:

// requestmanager.swift

class requestmanager: requesthandler

func sendrequest()

}// request.swift

@objc protocol requesthandler

class request

func gotresponse()

}

req 中以 weak 的方式持有了 delegate,因為網路請求是乙個非同步過程,很可能會遇到使用者不願意等待而選擇放棄的情況。這種情況下一般都會將 requestmanager 進行清理,所以我們其實是無法保證在拿到返回時作為 delegate 的 requestmanager 物件是一定存在的。因此我們使用了 weak 而非 unowned,並在呼叫前進行了判斷。

閉包和迴圈引用

另一種閉包的情況稍微複雜一些:我們首先要知道,閉包中對任何其他元素的引用都是會被閉包自動持有的。如果我們在閉包中寫了 self 這樣的東西的話,那我們其實也就在閉包內持有了當前的物件。這裡就出現了乙個在實際開發中比較隱蔽的陷阱:如果當前的例項直接或者間接地對這個閉包又有引用的話,就形成了乙個 self -> 閉包 -> self 的迴圈引用。最簡單的例子是,我們宣告了乙個閉包用來以特定的形式列印 self 中的乙個字串:

class

person

init(personname: string)

deinit

}var

xiaoming: person? = person(personname: "xiaoming")

xiaoming!.printname()

xiaoming = nil

// 輸出:

// the name is xiaoming,沒有被釋放

printname 是 self 的屬性,會被 self 持有,而它本身又在閉包內持有 self,這導致了 xiaoming 的 deinit 在自身超過作用域後還是沒有被呼叫,也就是沒有被釋放。為了解決這種閉包內的迴圈引用,我們需要在閉包開始的時候新增乙個標註,來表示這個閉包內的某些要素應該以何種特定的方式來使用。可以將 printname 修改為這樣:

lazy var

printname: ()->() =

}

現在記憶體釋放就正確了:

// 輸出:

// the name is xiaoming

// person deinit xiaoming

如果我們可以確定在整個過程中 self 不會被釋放的話,我們可以將上面的 weak 改為 unowned,這樣就不再需要 strongself 的判斷。但是如果在過程中 self 被釋放了而 printname 這個閉包沒有被釋放的話 (比如 生成 person 後,某個外部變數持有了 printname,隨後這個 persone 物件被釋放了,但是 printname 已然存在並可能被呼叫),使用 unowned 將造成崩潰。在這裡我們需要根據實際的需求來決定是使用 weak 還是 unowned。

這種在閉包引數的位置進行標註的語法結構是將要標註的內容放在原來引數的前面,並使用中括號括起來。如果有多個需要標註的元素的話,在同乙個中括號內用逗號隔開,舉個例子:

// 標註前

// 標註後

swift 記憶體管理

不管在什麼語言裡,記憶體管理的內容都很重要,所以我打算花上比其他 tip 長一些的篇幅仔細地說說這塊內容。swift 是自動管理記憶體的,這也就是說,我們不再需要操心記憶體的申請和分配。當我們通過初始化建立乙個物件時,swift 會替我們管理和分配記憶體。而釋放的原則遵循了自動引用計數 arc 的規...

Swift 記憶體管理

1 object c 經歷兩個階段 1 手動引用計數記憶體管理 manual reference counting,mrc 2 自動引用計數記憶體管理 automatic refernce counting,arc 2 引用型別 記憶體分配到 堆 上,需要人為管理。值型別 記憶體分配到 棧 上,有處...

Swift中的weak和unowned關鍵字

swift中沒有了strong,assign,copy關鍵字,對於所有的class型別變數都預設採用了strong型別,如果需要指定使用weak,則需要新增weak關鍵字修飾。正是由於這種預設的strong型別,在閉包中會引起迴圈引用,導致記憶體無法釋放,為了能夠在閉包 block 中正常釋放記憶體...