ARC 即 Automatic Reference Counts 自動(dòng)引用計(jì)數(shù),可以用于對(duì)象的自動(dòng)回收。
示例:定義一個(gè)User類,在初始化和銷毀時(shí)輸出打印語(yǔ)句
class User {
var name: String
init(name: String) {
self.name = name
print("初始化 \(name)")
}
deinit {
print("銷毀 \(name)")
}
}
// 使用
let user = User(name: "John")
// 打印結(jié)果
初始化 John
因?yàn)?strong>user始終在在作用域內(nèi),所以 deinit 不會(huì)被調(diào)用,可以將上面的語(yǔ)句包含在 do 語(yǔ)句中,這樣do語(yǔ)句完成后,就跳出該作用域了
do {
let user = User(name: "John")
}
// 打印結(jié)果
初始化 John
銷毀 John
這個(gè)例子主要是想表達(dá)一個(gè)對(duì)象正常情況下是如何初始化,如何被銷毀的
1.一個(gè)對(duì)象的生命周期
swift中對(duì)象的生命周期由5個(gè)階段組成:
-
Allocation: 從棧或堆中分配內(nèi)存 -
Initialization:init初始化 -
Usage:使用 -
Deinitialization:deinit銷毀 -
Deallocation: 將內(nèi)存返回給?;蚨眩M(jìn)行回收
Reference counts 也稱為 引用計(jì)數(shù),這個(gè)數(shù)量決定一個(gè)對(duì)象是否再需要,如果引用為0,則對(duì)象不再需要,會(huì)被銷毀。
上面示例中的 user 對(duì)象,引用計(jì)數(shù)如下:

2.引用循環(huán)(Reference Cycles)
大多數(shù)情況下,ARC會(huì)幫我們自動(dòng)處理對(duì)象的銷毀工作。
但是假如2個(gè)對(duì)象相互引用,因?yàn)?個(gè)對(duì)象的引用計(jì)數(shù)始終不為0,因此不會(huì)被銷毀。

上面圖示是一種 強(qiáng)引用循環(huán)(strong reference cycle)。
可以看出 object1 和 object2 相互引用,變量銷毀后,它們引用計(jì)數(shù)為 1, 因此2個(gè)對(duì)象不會(huì)被銷毀。
3.檢查你的引用(Checking Your References)
接著上面的例子,創(chuàng)建一個(gè) Phone 類
class User {
var name: String
// 用戶擁有的手機(jī)
private(set) var phones: [Phone] = []
init(name: String) {
self.name = name
print("初始化 \(name)")
}
func addPhone(phone: Phone) {
phones.append(phone)
// 手機(jī)的持有者
phone.owner = self
}
deinit {
print("銷毀 \(name)")
}
}
class Phone {
let model: String
// 手機(jī)的主人
var owner: User?
init(model: String) {
self.model = model
print("手機(jī)模型 \(model)")
}
deinit {
print("銷毀手機(jī)模型 \(model)")
}
}
do {
let user = User(name: "John")
let iphone = Phone(model: "iphone6s")
user.addPhone(phone: iphone)
}
在 Playground 中運(yùn)行上面的代碼,會(huì)打印
初始化 John
手機(jī)模型 iphone6s
會(huì)發(fā)現(xiàn) user 和 iphone 并沒有被回收,兩者之間建立了強(qiáng)引用循環(huán),導(dǎo)致對(duì)象不會(huì)被回收

4. 弱引用(Weak References)
為了打破強(qiáng)引用循環(huán),可以將相互引用的對(duì)象之間的關(guān)系設(shè)置為 弱(weak)。
除非特別的指定,所有的引用都是強(qiáng)引用,會(huì)影響引用計(jì)數(shù)。弱引用則不會(huì)增加對(duì)象的引用計(jì)數(shù),另外:
- 弱引用永遠(yuǎn)都要聲明為可選類型,因此必須使用
var聲明 - 當(dāng)引用數(shù)變?yōu)?時(shí),引用會(huì)自動(dòng)的設(shè)置為
nil

上圖中虛線表示弱引用,實(shí)線表示強(qiáng)引用。 object1 有1個(gè)強(qiáng)引用,一個(gè)弱引用(object2 對(duì) object1 是弱引用), object2 有2個(gè)強(qiáng)引用。
回到 Phone 類中,將 owner 屬性聲明為 weak
class User {
var name: String
private(set) var phones: [Phone] = []
init(name: String) {
self.name = name
print("初始化 \(name)")
}
func addPhone(phone: Phone) {
phones.append(phone)
// 手機(jī)的持有者
phone.owner = self
}
deinit {
print("銷毀 \(name)")
}
}
class Phone {
let model: String
// 將phone對(duì)user的引用設(shè)置為弱引用
weak var owner: User?
init(model: String) {
self.model = model
print("手機(jī)模型 \(model)")
}
deinit {
print("銷毀手機(jī)模型 \(model)")
}
}
do {
let user = User(name: "John")
let iphone = Phone(model: "iphone6s")
user.addPhone(phone: iphone)
}
打印結(jié)果
初始化 John
手機(jī)模型 iphone6s
銷毀 John
銷毀手機(jī)模型 iphone6s

5. 無(wú)主引用(Unowned References)
這是另一種不增加引用計(jì)數(shù)的修飾符:unowned.
unowned 和 weak 有什么區(qū)別呢?弱引用必須為可選類型,當(dāng)引用的對(duì)象不存在時(shí),自動(dòng)設(shè)置為 nil; 無(wú)主引用則不能為可選類型,如果你引用一個(gè)已經(jīng)被銷毀的對(duì)象屬性,會(huì)拋出錯(cuò)誤。

在上面的代碼中添加一個(gè) CarrierSubscription (運(yùn)營(yíng)商訂閱者)類
class User {
var name: String
private(set) var phones: [Phone] = []
// 使用的運(yùn)營(yíng)商
var subscriptions: [CarrierSubscription] = []
init(name: String) {
self.name = name
print("初始化 \(name)")
}
func addPhone(phone: Phone) {
phones.append(phone)
// 手機(jī)的持有者
phone.owner = self
}
deinit {
print("銷毀 \(name)")
}
}
class Phone {
let model: String
weak var owner: User?
var carrierSubscription: CarrierSubscription?
init(model: String) {
self.model = model
print("手機(jī)模型 \(model)")
}
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
func decommission() {
self.carrierSubscription = nil
}
deinit {
print("銷毀手機(jī)模型 \(model)")
}
}
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
let user: User
init(name: String, countryCode: String, number: String, user: User) {
// 運(yùn)營(yíng)商名
self.name = name
// 區(qū)號(hào)
self.countryCode = countryCode
// 電話號(hào)碼
self.number = number
// 對(duì)應(yīng)用戶
self.user = user
// 將 CarrierSubscription 添加到User的subscriptions屬性中
user.subscriptions.append(self)
print("CarrierSubscription \(name) 初始化")
}
deinit {
print("CarrierSubscription \(name) 銷毀")
}
}
do {
let user = User(name: "John")
let iphone = Phone(model: "iphone6s")
user.addPhone(phone: iphone)
let subscription = CarrierSubscription(
name: "TelBel",
countryCode: "0032",
number: "31415926",
user: user)
iphone.provision(carrierSubscription: subscription)
}
// 打印結(jié)果
初始化 John
手機(jī)模型 iphone6s
CarrierSubscription TelBel 初始化
可以發(fā)現(xiàn) user & iphone & subscription 最后都沒有被回收.
它們3者之間的引用關(guān)系如下

打破這種關(guān)系(Break the Chain)
一個(gè) User 可以不需要一個(gè) CarrierSubscription 存在 ,但是,運(yùn)營(yíng)商不能沒有 User 的情況下存在,因此 CarrierSubscription 的 user 應(yīng)該是 無(wú)主的 (unowned)。
將CarrierSubscription 的 user 進(jìn)行更改
let user: User
// 更改為
unowned let user: User
然后運(yùn)行代碼, 打印結(jié)果如下,可以看出3個(gè)對(duì)象都被銷毀了:
初始化 John
手機(jī)模型 iphone6s
CarrierSubscription TelBel 初始化
銷毀 John
銷毀手機(jī)模型 iphone6s
CarrierSubscription TelBel 銷毀
引用關(guān)系圖如下:

6. 閉包引起的引用循環(huán)(Reference Cycles with Closures)
因?yàn)殚]包也是 引用類型,因此也會(huì)導(dǎo)致引用循環(huán)的問(wèn)題。
比如,如果你將閉包賦值給一個(gè)類中的某個(gè)屬性,該閉包使用到了實(shí)例的某些屬性,這樣就會(huì)導(dǎo)致引用循環(huán)。即:對(duì)象通過(guò)一個(gè)存儲(chǔ)屬性引用一個(gè)閉包,閉包通過(guò) self 的捕獲值(captured value) 引用了對(duì)象。

在上面的 CarrierSubscription 中添加一個(gè)屬性
// completePhoneNumber是存儲(chǔ)屬性 它引用了一個(gè)閉包
lazy var completePhoneNumber: () -> String = {
// 閉包內(nèi)部引用了對(duì)象的某些屬性
return self.countryCode + " " + self.number
}
在 do 語(yǔ)句中添加
print(subscription.completePhoneNumber())
即:
class User {
var name: String
private(set) var phones: [Phone] = []
// 使用的運(yùn)營(yíng)商
var subscriptions: [CarrierSubscription] = []
init(name: String) {
self.name = name
print("初始化 \(name)")
}
func addPhone(phone: Phone) {
phones.append(phone)
// 手機(jī)的持有者
phone.owner = self
}
deinit {
print("銷毀 \(name)")
}
}
class Phone {
let model: String
weak var owner: User?
var carrierSubscription: CarrierSubscription?
init(model: String) {
self.model = model
print("手機(jī)模型 \(model)")
}
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
func decommission() {
self.carrierSubscription = nil
}
deinit {
print("銷毀手機(jī)模型 \(model)")
}
}
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
unowned let user: User
lazy var completePhoneNumber: () -> String = {
return self.countryCode + " " + self.number
}
init(name: String, countryCode: String, number: String, user: User) {
// 運(yùn)營(yíng)商名
self.name = name
// 區(qū)號(hào)
self.countryCode = countryCode
// 電話號(hào)碼
self.number = number
// 對(duì)應(yīng)用戶
self.user = user
// 將 CarrierSubscription 添加到User的subscriptions屬性中
user.subscriptions.append(self)
print("CarrierSubscription \(name) 初始化")
}
deinit {
print("CarrierSubscription \(name) 銷毀")
}
}
do {
let user = User(name: "John")
let iphone = Phone(model: "iphone6s")
user.addPhone(phone: iphone)
let subscription = CarrierSubscription(
name: "TelBel",
countryCode: "0032",
number: "31415926",
user: user)
iphone.provision(carrierSubscription: subscription)
print(subscription.completePhoneNumber())
}
// 打印結(jié)果
初始化 John
手機(jī)模型 iphone6s
CarrierSubscription TelBel 初始化
0032 31415926
銷毀 John
銷毀手機(jī)模型 iphone6s
可以發(fā)現(xiàn) user & iphone 對(duì)象都被回收了,但是 subscription 沒有。這是由于對(duì)象和閉包之間的強(qiáng)引用:

捕獲列表(Capture Lists)
swift 有一種簡(jiǎn)潔而且優(yōu)雅的方式解決閉包中的強(qiáng)引用。 在閉包中聲明一個(gè)捕獲列表,用來(lái)定義它和捕獲對(duì)象之間的關(guān)系。
簡(jiǎn)單示例:
var x = 5
var y = 5
// 將 x 放入到捕獲列表中
// 這樣可以在閉包定義的位置對(duì)x的值進(jìn)行捕獲, 這里x的值為5
// 捕獲是通過(guò)值進(jìn)行的 而不是引用
let someClosure = { [x] in
print("\(x), \(y)")
}
x = 6
y = 6
someClosure() // 打印 5,6
print("\(x), \(y)") // 打印 6,6
捕獲列表在閉包中對(duì)使用的對(duì)象定義 weak 或者 unowned 關(guān)系使用方便。
在上面的 CarrierSubscription 示例中,使用 unowned 定義捕獲列表比較合適,因?yàn)殚]包在 CarrierSubscription 銷毀后也不存在了。
因此將上面的閉包更改:
lazy var completePhoneNumber: () -> String = {
return self.countryCode + " " + self.number
}
// 更改為
// 對(duì) self 進(jìn)行捕獲
// 關(guān)系為 unowned 這樣CarrierSubscription銷毀后 閉包也不復(fù)存在
lazy var completePhoneNumber: () -> String = { [unowned self] in
return self.countryCode + " " + self.number
}
// 打印結(jié)果
初始化 John
手機(jī)模型 iphone6s
CarrierSubscription TelBel 初始化
0032 31415926
銷毀 John
銷毀手機(jī)模型 iphone6s
CarrierSubscription TelBel 銷毀
可以看出3個(gè)對(duì)象都被銷毀了。
上面的語(yǔ)法實(shí)質(zhì)上是一種簡(jiǎn)寫:
lazy var completePhoneNumber: () -> String = { [unowned newID = self] in
return newID.countryCode + " " + newID.number
}
newID 是一個(gè) unowned self 的拷貝。在閉包外的作用域, self 保持自己原有的意義。
6.1 小心使用無(wú)主關(guān)系(Using Unowned with Care)
上面的示例中 self 和 completePhoneNumber 的關(guān)系是 unowned.
如果你確認(rèn)閉包中的引用對(duì)象永遠(yuǎn)不會(huì)被回收,則可以使用 unowned, 如果 self 被回收了,則會(huì)報(bào)錯(cuò)。
比如
class WWDCGreeting {
let who: String
init(who: String) {
self.who = who
}
// 使用 [unowned self] 表示self 和 閉包之間的關(guān)系
lazy var greetingMaker: () -> String = { [unowned self] in
return "Hello, \(self.who)"
}
}
let greetingMaker: () -> String
do {
let john = WWDCGreeting(who: "John")
greetingMaker = john.greetingMaker
}
// 報(bào)錯(cuò) 因?yàn)?self.who 已經(jīng)被回收
print(greetingMaker())
直接報(bào)錯(cuò),因?yàn)殚]包期望 self.who 仍然有效,但是當(dāng) john 在作用域之外后,你對(duì)其進(jìn)行了回收。
解決這個(gè)陷阱
使用 weak 替換 unowned
class WWDCGreeting {
let who: String
init(who: String) {
self.who = who
}
// 將 [unowned self] 替換為 [weak self]
lazy var greetingMaker: () -> String = { [weak self] in
// 使用 self?.who 替換 self.who
return "Hello, \(self?.who)"
}
}
let greetingMaker: () -> String
do {
let john = WWDCGreeting(who: "John")
greetingMaker = john.greetingMaker
}
// 打印 Hello, nil
print(greetingMaker())
對(duì)上面的閉包進(jìn)一步優(yōu)化
lazy var greetingMaker: () -> String = { [weak self] in
guard let self = self else {
return "No greeting available."
}
return "Hello \(self.who)."
}
7. 值類型循環(huán)和引用類型循環(huán)(Cycles with Value Types and Reference Types)
swift 中class是引用類型,sturct,enum等是值類型。當(dāng)傳遞值類型時(shí),是進(jìn)行拷貝操作,而引用類型則是進(jìn)行共享。
// 錯(cuò)誤用法
// 因?yàn)榻Y(jié)構(gòu)體是值類型,不能遞歸使用自己或者使用自己的實(shí)例
struct Node {
var payload = 0
var next: Node? // 使用自己類型 報(bào)錯(cuò)
}
但是對(duì)于class,自我引用不會(huì)報(bào)錯(cuò)
class Person {
var name: String
// friends 是 [Person] 類型
var friends: [Person] = []
init(name: String) {
self.name = name
print("新的 person實(shí)例: \(name)")
}
deinit {
print("person實(shí)例 \(name) 被銷毀")
}
}
do {
let john = Person(name: "John")
let harry = Person(name: "Harry")
// 形成循環(huán)引用
john.friends.append(harry)
harry.friends.append(john)
}
john 和 harry 因?yàn)?friends 數(shù)組的原因,導(dǎo)致相互引用,盡管array自身是值類型。如果將 friends 聲明為 unowned let friends, Xcode會(huì)拋出錯(cuò)誤: unowned 只能用于 class types.
為了解決這個(gè)問(wèn)題,需要參加一個(gè)泛型的對(duì)象包裹,然后用它給數(shù)組添加實(shí)例。
class Unowned<T: AnyObject> {
unowned var value: T
init(_ value: T) {
self.value = value
}
}
class Person {
var name: String
// friends 是 Unowned<Person> 類型 對(duì)Person進(jìn)行包裹
var friends: [Unowned<Person>] = []
init(name: String) {
self.name = name
print("新的 person實(shí)例: \(name)")
}
deinit {
print("person實(shí)例 \(name) 被銷毀")
}
}
do {
let john = Person(name: "John")
let harry = Person(name: "Harry")
john.friends.append(Unowned(harry))
harry.friends.append(Unowned(john))
}
這樣所有的對(duì)象都可以被銷毀。
注意,friends不再是一個(gè) Person 獨(dú)享集合,而是一個(gè) Unowned 對(duì)象集合,用于對(duì) Person實(shí)例進(jìn)行包裹。
為了訪問(wèn) Person 對(duì)象,可以使用 Unowned 中定義的 value 屬性
let firstFriendOfHarry = harry.friends.first?.value
這篇文章主要講了以下幾個(gè)知識(shí)點(diǎn):
- 對(duì)象之間的幾種關(guān)系,強(qiáng),弱,無(wú)主
weakunowned
- 捕獲列表的用法
- 捕獲列表在閉包中如何消除引用循環(huán)
- 弱引用關(guān)系的使用方式
- 一定是可選類型,使用
var進(jìn)行申明 - 引用計(jì)數(shù)為0時(shí),自動(dòng)設(shè)置為
nil
- 一定是可選類型,使用
- 無(wú)主引用使用的注意事項(xiàng)
- 一定要確保使用期間對(duì)象不能被回收
- 值類型引用循環(huán)和引用類型循環(huán),如何使用 泛型對(duì)象 對(duì)類型進(jìn)行包裝,解決相互引用的問(wèn)題
文章來(lái)源:
2019年05月20日00:43:43