今回は、クロージャーの[weak self]
について書いていこうと思います。
なぜこんな書き方をするのか調べてみると、クロージャーがselfを弱参照し、クロージャーとselfの循環参照を防ぐとの記載がありました。
これを見て??って思いよくわからなかったので、少し調べていきたいと思います。
前提となる参照カウンタなどについては下記で触れています。見ておくと理解が深まるかもしれません。
循環参照とは?
まずは、循環参照について調べていきたいと思います。
循環参照とは、インスタンス間でお互いを強参照しあった場合に参照カウントが0にならず、メモリ上にインスタンスが残り続けてしまう状態のことです。
Swiftでは構造体や列挙隊以外は基本的に強参照です。
class Human { let name: String var money: Money? init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } } class Money { let amount: Int var owner: Human? init(amount: Int) { self.amount = amount print("\(amount) is being initialized") } deinit { print("\(amount) is being deinitialized") } }
上記のような2つのクラスがあるとします。初期化すると以下のようなイメージになります。
var kuma: Human? = Human(name: "kuma") // kumaの参照カウントが+1 var money: Money? = Money(amount: 1000) // moneyの参照カウントが+1
この状態で循環参照を実行します。イメージは下記です。
kuma?.money = money // moneyの参照カウントが+1 money?.owner = kuma // kumaの参照カウントが+1 kuma = nil // kumaの参照カウントが-1 money = nil // moneyの参照カウントが-1
循環参照を行うと参照カウントが0にならず、kuma
もmoney
もdeinit
されません。結果的にメモリリークを引き起こします。
循環参照しないためには?
では、循環参照しないためにはどうすればいいでしょうか?
循環参照しないためには参照カウントを増やさなければいいのです。 そのために弱参照(weak reference)とアンオウンド参照(unowned reference)の2種類の参照方法があります。
弱参照は参照するインスタンスが後からnilになる場合に使用します。また、通常のOptionalと同様にアンラップが必要になります。
一方、アンオウンド参照は設定されたらnilになることがない(インスタンスの存続期間が一方またはお互いに依存する)場合に使用します。
class Human { let name: String var money: Money? init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } } class Money { let amount: Int weak var owner: Human? //weak(弱参照にする) // 省略 }
上記のようにどちらかの参照をweak
もしくはunowned
にします。これによって循環参照は解消され、不要なインスタンスは破棄されます。
var kuma: Human? = Human(name: "kuma") // kumaの参照カウントが+1 var money: Money? = Money(amount: 1000) // moneyの参照カウントが+1 kuma?.money = money // moneyの参照カウントが+1 money?.owner = kuma // kumaの参照カウントは変わらない kuma = nil // kumaの参照カウントが-1 money = nil // moneyの参照カウントが-1 kuma = nil // kumaの参照カウントが-1, kumaのdeinitが呼ばれ、moneyの参照カウントが-1 money = nil // moneyの参照カウントが-1, moneyのdeinitが呼ばれる
イメージは下記です。
クロージャの循環参照
クロージャもインスタンスなので、クロージャ内でインスタンスを利用した際に循環参照が起こる場合があります。
クラスのプロパティにクロージャを割り当て、その中でself
を参照する場合です。実際に見ていきましょう!
class Human { let name: String let blood: String init(name: String, blood: String) { self.name = name self.blood = blood } lazy var getHumanInfo: () -> String = { return "\(self.name) \(self.blood)" // selfの参照カウントが+1 } } var kuma: Human? = Human(name: "kuma", blood: "A") print(john?.getHumanInfo()) // getHumanInfoの参照カウントが+1とselfの参照カウントが+1の合計+2になる kuma = nil // Human内で循環参照が起きているのでdeinitが呼ばれない
上記の場合は、Human
インスタンスがgetHumanInfo
インスタンスを参照しており、getHumanInfo
インスタンスからHuman
インスタンスを参照しているので、循環参照が発生しHuman
インスタンスが破棄されません。
循環参照を避けるために、クロージャ内でインスタンスを利用する場合にはキャプチャリストを用います。 キャプチャリストでは弱参照・アンオウンド参照を指定することができるので、 クロージャの解放状況に依存せずにクラスのインスタンスを解放することが可能になります。
class Human { let name: String let blood: String // 省略 lazy var getHumanInfo: () -> String = { [weak self] _ in return "\(self.name) \(self.blood)" // selfの参照カウントは変わらない } } var kuma: Human? = Human(name: "kuma", blood: "A") print(john?.getHumanInfo()) // getHumanInfoの参照カウントが+1 kuma = nil // deinitが呼ばれる
キャプチャリストとは上記の場合、[weak self]
のことです。in
は必ず必要です。
メモリ管理でのバグは調査が大変だと思うので、困ったら[weak self]
を使い、[unowned self]
を使う場合は設計を確認して使うのがいい気がしました。