くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

クロージャーの[weak self]について

今回は、クロージャーの[weak self]について書いていこうと思います。

なぜこんな書き方をするのか調べてみると、クロージャーがselfを弱参照し、クロージャーとselfの循環参照を防ぐとの記載がありました。

これを見て??って思いよくわからなかったので、少し調べていきたいと思います。

前提となる参照カウンタなどについては下記で触れています。見ておくと理解が深まるかもしれません。

値型(struct)と参照型(class)について

循環参照とは?

まずは、循環参照について調べていきたいと思います。

循環参照とは、インスタンス間でお互いを強参照しあった場合に参照カウントが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

f:id:kumaskun:20200927122251p:plain
初期化後のイメージ

この状態で循環参照を実行します。イメージは下記です。

kuma?.money = money // moneyの参照カウントが+1
money?.owner = kuma // kumaの参照カウントが+1
kuma = nil // kumaの参照カウントが-1
money = nil // moneyの参照カウントが-1

f:id:kumaskun:20200927122834p:plain
循環参照のイメージ

循環参照を行うと参照カウントが0にならず、kumamoneydeinitされません。結果的にメモリリークを引き起こします。

循環参照しないためには?

では、循環参照しないためにはどうすればいいでしょうか?

循環参照しないためには参照カウントを増やさなければいいのです。 そのために弱参照(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が呼ばれる

イメージは下記です。

f:id:kumaskun:20200927125149p:plain

クロージャの循環参照

クロージャインスタンスなので、クロージャ内でインスタンスを利用した際に循環参照が起こる場合があります。

クラスのプロパティにクロージャを割り当て、その中で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]を使う場合は設計を確認して使うのがいい気がしました。

参照

Swiftでなんで[weak self]するのか?

Swiftのメモリ管理を知る