LoginSignup
164
148

More than 5 years have passed since last update.

【Swift】ハマりがちな循環参照について

Last updated at Posted at 2015-05-14

iOSアプリを作ってると気づかないうちにやってしまっている循環参照。
メモリリークの原因は大概コレが原因となっていると思います。

1.循環参照とは?

循環参照とは「クラスAとクラスBのインスタンスがそれぞれ存在した時、クラスAのインスタンスをクラスBのプロパティに格納し、クラスBのインスタンスをクラスAのプロパティに格納する」といった状態になった時に、お互いにインスタンスを参照しあうため、どちらも解放されずにそのまま残り続けてしまう、という現象です。

2.強参照と弱参照

クラスのインスタンスは何も指定しなければ通常「強参照」と呼ばれる形で代入されます。
強参照とは、端的に言えば親子関係です。
親が解放されると子も自動的に解放されます。
ここで上記のようにお互いが親としてインスタンスを保持すると循環参照が起きるわけです。

そこで、この循環参照が起こることを防ぐために弱参照を使います。
Swiftにはweakとunownedといった修飾子がこの為に用意されています。
強参照が親子関係だったのに対して、弱参照はインスタンスの使用権を得るだけです。
つまり、親が解放されてしまうと弱参照で代入されたインスタンスも使用してようがしてまいが解放され、参照することができなくなります。

3.クロージャで発生する循環参照

selfが保持しているクロージャの内部でselfを参照すると循環参照が起きます。
例えば、下記の場合循環参照が発生します。

(15/6/21)例で誤りがありましたので修正しました

class Sample{
    var count : Int = 0
    var inc: (() -> Int)! = nil
    init() {
        inc = incrementCount()
    }
    func incrementCount()->()->Int{
        return{
            ++(self.count)
        }
    }
    deinit {
        println("deinit")
    }
}
var sample:Sample? = Sample()
sample = nil
//deinitがよばれない=インスタンスが解放されていない

これを防ぐ為に、先ほど紹介したweakやunownedを使用します。

class Sample{
    var count : Int = 0
    var inc: (() -> Int)! = nil
    init() {
        inc = incrementCount()
    }
    func incrementCount()->()->Int {
        return {
            [unowned self] in
            ++(self.count)
        }
    }
    deinit {
        println("deinit")
    }
}
var sample:Sample? = Sample()
sample = nil
deinit //deinitがよばれた!

4.weakとunownedの違い

weakを用いると、インスタンスが解放された時nilが入ります。
よって、nilが代入される可能性があるのでweakを使うときはOptionalでないといけません。
一方、unownedで宣言された変数にはnilにはなりません。
よって、既に親によってインスタンスが解放されていた場合、unownedを参照すると落ちます。

結論を言うと、基本的にはweakを使っておけば問題になることはないと思います。
ただ、インスタンスが必ず存在すると保証できる時にweakを使うとアンラップが必要な分、記述が冗長になります。

class Sample{
    var count : Int = 0
    var inc: (() -> Int)! = nil
    init() {
        inc = incrementCount()
    }
    func incrementCount()->()->Int{
        return{
            [weak self] () ->Int in
            if let weakSelf = self{
                return ++(weakSelf.count)
            }
            return 0
        }
    }
}

クロージャ内でselfを参照する場合は、selfが先に解放されてしまうということはあまりないのでunownedでも大概問題にはならないと思います。
逆に、こういうケース以外でunownedを使う時は慎重にならないといけません。

更に、selfが先に解放されてしまう、というケースも存在します。
例えば、非同期処理においてコールバック関数が呼ばれた時には、既にselfが解放されてしまっている場合などが考えられます。

164
148
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
164
148