【Swift】強参照、循環参照、弱参照、アンオウンド参照

なんとなくだった部分を下記サイトを参考に簡単に整理してみる。
というかARCがない時に開発してたときは大変そうだなと思った。。

参考
ARC | Swift言語を学ぶ

◼︎強参照とは?

ざっくり言うと

  • 参照カウントを増減させるような参照。
  • 基本的にクラスのインスタンスは強参照
  • 変数にnilが入るとカウントが1減る
  • 参照カウントが0になるとインスタンスも破棄される

class SmartPhone {
   private let producer: String?
    
    init(producer: String) {
        self.producer = producer
    }
    
    deinit {
        print("SmartPhone deinit")
    }
    
}

var iPhone5: SmartPhone? = SmartPhone(producer: "Steve Jobs") //参照カウント1
var iPhone6: SmartPhone?

iPhone6 = iPhone5 //参照カウント2
iPhone5 = nil //参照カウント1
iPhone6 = nil //参照カウント0 ここでdeinitが呼ばれる

しかし、次のようなときは

class SmartPhone {
    private let producer: String?
    
    init(producer: String) {
        self.producer = producer
    }
    
    deinit {
        print("SmartPhone deinit")
    }
    
}

class Owner {
    var smartPhone: SmartPhone?
    
    init(smartPhone: SmartPhone) {
        self.smartPhone = smartPhone
    }
    deinit {
        print("Owner deinit")
    }
}

var iPhone5: SmartPhone? = SmartPhone(producer: "Steve Jobs") //参照カウント1
var iPhone6: SmartPhone?
var owner: Owner? = Owner(smartPhone: iPhone5!) //参照カウント2

iPhone6 = iPhone5 //参照カウント3
iPhone5 = nil //参照カウント2
iPhone6 = nil //参照カウント1 
print("iPhone5: \(iPhone5)") //nil
print("iPhone6: \(iPhone6)") // nil
print("owner.smartPhone: \(owner?.smartPhone)") //Optional(SmartPhone)

となり、ownerのsmartPhoneは強参照なのでSmartPhoneクラスのdeinitは呼ばれない。

◼︎循環参照とは?

関連する複数のクラスのインスタンス同士をお互いに
強参照で保持すると、循環参照となりメモリリークする場合がある。

//スマートフォンクラス
class SmartPhone {
    private let producer: String
    var owner: Owner?
    
    init(producer: String) {
        self.producer = producer
    }
    
    deinit {
        print("SmartPhone deinit")
    }
    
}

//所有者クラス
class Owner {
    var smartPhone: SmartPhone?
    let fingerPrint: String
    
    init(fingerPrint: String) {
        self.fingerPrint = fingerPrint
    }
    deinit {
        print("Owner deinit")
    }
}

var iPhone6: SmartPhone? = SmartPhone(producer: "Steve Jobs")
var owner: Owner? = Owner(fingerPrint: "ownerの指紋")
iPhone6?.owner = owner
owner?.smartPhone = iPhone6

iPhone6 = nil
owner = nil

最後にそれぞれのインスタンスnilをいれてもスマートフォンクラスと所有者クラスのインスタンスがお互いにそれぞれのインスタンスを保持し合っているため、参照カウントが0にならずメモリに残ったままになっている。それぞれのインスタンスにはnilが代入されているので、これらのインスタンスにアクセスする手段もなく、そのうちメモリリークしてしまう。

循環参照を防ぐためには

  • 弱参照
  • アンオウンド参照

がある。
参照するインスタンスが後からnilになる可能性がある場合は弱参照。
二度とnilにならないのであればアンオウンド参照。

◼︎弱参照

弱参照の場合は先ほどの循環参照の例のSmartPhoneクラスの

var owner: Owner?

weak var owner: Owner?

にすれば良い。
こうすれば参照カウントにカウントされずに参照することができる。

アクセスするときはnilチェックで安全に。

if let owner = iPhone6?.owner {
    print(owner.fingerPrint)
}


◼︎アンオウンド参照
ちょっと例が良くないかもだけどスマートフォンは必ず誰かに持たれてるという前提があるする。

//スマートフォンクラス
class SmartPhone {
    var producer: String?
    let udid: String
    unowned var owner: Owner
    
    init(udid: String, owner: Owner) {
        self.udid = udid
        self.owner = owner
    }
    
    deinit {
        print("SmartPhone deinit")
    }
    
}

//所有者クラス
class Owner {
    var smartPhone: SmartPhone!
    let fingerPrint: String
    
    init(fingerPrint: String) {
        self.fingerPrint = fingerPrint
    }
    deinit {
        print("Owner deinit")
    }
}

Ownerはスマホを持ちあるかない可能性があるが、スマホには必ず所有者がいるので所有者がnilになることはない。そのためownerをunownedをつけて宣言する。ここではイニシャライザでselfにアクセスする必要があるので、スマートフォンプロパティを「暗黙的なオプショナル型」で宣言する必要がある。

クロージャの循環参照

クロージャを使用する場合もselfを使ってインスタンスのプロパティにアクセスする場合などにインスタンスクロージャの間で循環参照する場合がある。
例えばselfを弱参照、アンオウンド参照したい時は

//弱参照
{ (parameters) -> [weak self] return type in
    statements
}

//アンオウンド参照
{ (parameters) -> [unowned self] return type in
    statements
}

のように、引数の前に指定してあげればよい。