Swift で循環参照するケース

循環参照という言葉を最近よく耳にしていて、なんとなく相互参照の結果メモリリークしてしまうというのは分かっていたのですが、じゃあどうしたら起きてしまうのかというところがあやふやだったので調べてみました。

循環参照

インスタンスは参照カウンタという値を持っており、変数に代入されたりして強参照されることで参照カウンタが 1 増えます。この参照カウンタが 0 になるとインスタンスは解放されます。

class Sample {}

// Sample クラスのインスタンスが生成されるが参照カウンタが 0 のままなのですぐに解放される
Sample()

// Sample クラスのインスタンスが生成されて sample に代入されているので参照カウンタが 1 となり解放されない
var sample: Sample? = Sample()

// nil を代入すると参照がなくなるのでインスタンスの参照カウンタが 0 になり解放される
sample = nil

循環参照とは、参照カウンタが 0 になり得ない状態をつくってしまう参照のことです。
これにより、インスタンスは解放されることなく残り続けてしまい、メモリリークとなってしまいます。

具体的なケースをいくつか書いてみます。 ( Swift 3.0 )
図では、赤枠が変数、青枠がインスタンス、緑枠がクロージャを表しています。

ケース1

f:id:komaji504:20161218022711p:plain:w500

class Computer {
    var printer: Printer?
}

class Printer {
    var computer: Computer?
}

var computer: Computer? = Computer() // Computer の参照カウンタ1
var printer: Printer? = Printer() // Printer の参照カウンタ1

computer.printer = printer // Printer の参照カウンタ2
printer.computer = computer // Computer の参照カウンタ2

computer = nil // Computer の参照カウンタ1
printer = nil // Printer の参照カウンタ1

// 各インスタンスの参照カウンタは1なので解放されていない
// しかしプログラムから参照することはできない

インスタンスのプロパティで一方のインスタンスを強参照しているので、変数からの参照がなくなった後も各インスタンスは解放されません。
変数からインスンタンスへの参照がなくなったことによりプログラムからインスタンスを参照することができなくなってしまい、無駄なインスタンスが生じている状態になっています。

対処例

このケースはプロパティに対して弱参照を用いることで解決できます。

class Computer {
    var printer: Printer?
}

class Printer {
    // weak をつけることで弱参照となる
    weak var computer: Computer?
}

var computer: Computer? = Computer() // Computer の参照カウンタ 1
var printer: Printer? = Printer() // Printer の参照カウンタ 1

computer.printer = printer // Printer の参照カウンタ 2
printer.computer = computer // Computer の参照カウンタ 1
// 弱参照は参照カウンタを増やさない

computer = nil // Computer の参照カウンタ 0 , Printer の参照カウンタ 1
// computer が解放されることで computer.printer からの Printer への参照もなくなる

printer = nil // Printer の参照カウンタ 0

weak をつけてプロパティ定義をすると、そのプロパティからインスタンスへの参照は弱参照となります。
弱参照とは、参照カウンタを増やさずに参照する方法で、これにより循環参照を防ぐことができます。

delegate を実装するときに delegate プロパティに対して weak をよくつけるかと思いますが、それはこのケースのためです。

ケース2

f:id:komaji504:20161218022731p:plain:w200

class Printer {
    var printClosure: (() -> Void)
    
    init() {
        self.printClosure = {
            print("Printer name is", self)
        }
    }
    
    func run() {
        printClosure?()
    }
}

var printer: Printer? = Printer() // Printer の参照カウンタ 2 , クロージャの参照カウンタ 1

printer = nil // Printer の参照カウンタ 1

クロージャは、生成されたタイミングでクロージャ内で扱われているインスタンスへの参照をキャプチャします。つまり、クロージャが解放されるまで参照が保持されるので、強参照をしている場合はインスタンスの参照カウンタが 1 増えたままということになります。

そのため、上記のケースでは、 Printer のインスタンスが生成されたときに self ( = Printer ) を強参照するクロージャが生成されているので、 Printer のインスタンスの参照カウントが 1 になります。

そして、このクロージャをプロパティへ代入しているため、 Printer のインスタンスクロージャを強参照し、クロージャもまた Printer のインスタンスを強参照しているので循環参照となっています。

対処例

ケース1と同様にプロパティを弱参照とすることもできますが、クロージャがキャプチャする参照を弱参照へと変更する方法もあります。
その場合は以下のように [weak self] と記述します。

class Printer {
    var printClosure: (() -> Void)
    
    init() {
        // self の参照を弱参照にする
        self.printClosure = { [weak self] in
            print("Printer name is", self ?? "")
        }
    }
    
    func run() {
        printClosure?()
    }
}

var printer: Printer? = Printer() // Printer の参照カウンタ 1 , クロージャの参照カウンタ 1

printer = nil // Printer の参照カウンタ 0, クロージャの参照カウンタ 0

これにより循環参照を防ぐことができます。
しかし、弱参照とすることで参照先のインスタンスが解放されることを許容することになるので、クロージャ内ではオプショナル型として扱われます。上記の場合では、 self はオプショナル型となっています。そのため、オプショナルバインディング等でアンラップする必要があります。

unowned

ちなみに unowned というのもあり、これをつけることで参照カウンタを増やさずに参照することができます。
weak との違いは、 オプショナル型と暗黙的オプショナル型の違いのように、 参照先が解放されてしまっている ( nil になっている ) ときに実行時エラーとなるかどうかということのようです。

weak は実行時エラーになりませんが、 unowned は実行時エラーとなります。

ですが、 unowned の場合はオプショナルバインディング等でアンラップする必要がありません。

ケース3

f:id:komaji504:20161219002036p:plain:w500

class Computer {
    var printer: Printer?
    
    func printName() {
        printer?.run {
            print("Computer name is", self)
        }
    }
}

class Printer {
    var printClosure: (() -> Void)?
    
    func run(printClosure: @escaping () -> Void) {
        self.printClosure = printClosure
        run()
    }
    
    func run() {
        printClosure?()
    }
}

var computer: Computer? = Computer() // Computer の参照カウンタ 1
var printer: Printer? = Printer() // Printer の参照カウンタ 1

computer?.printer = printer // Printer の参照カウンタ 2
computer?.printName() // Computer の参照カウンタ 2 , クロージャの参照カウンタ 1

computer = nil // Computer の参照カウンタ 1
printer = nil // Printer の参照カウンタ 1

このケースでは、 func printName() 実行した時点で循環参照となってしまっています。
このメソッド内では クロージャを受け取るメソッド func run(printClosure: @escaping () -> Void) を実行しているのですが、渡したクロージャが Printer のプロパティへと代入されてしまっているのでクロージャへの強参照が生じています。

これにより、クロージャが Computer のインスタンスself で強参照する、 Computer のインスタンスがプロパティで Printer のインスタンスを強参照する、 Printer のインスタンスがプロパティでクロージャを強参照するという循環参照となってしまっています。

対処例

class Computer {
    var printer: Printer?
    
    // self の参照を弱参照にする
    func printName() { [weak self] in
        printer?.run {
            print("Computer name is", self ?? "")
        }
    }
}

class Printer {
    var printClosure: (() -> Void)?
    
    func run(printClosure: @escaping () -> Void) {
        self.printClosure = printClosure
        run()
    }
    
    func run() {
        printClosure?()
    }
}

var computer: Computer? = Computer() // Computer の参照カウンタ 1
var printer: Printer? = Printer() // Printer の参照カウンタ 1

computer?.printer = printer // Printer の参照カウンタ 2
computer?.printName() // Computer の参照カウンタ 1 , クロージャの参照カウンタ 1

computer = nil // Computer の参照カウンタ 0 , Printer の参照カウンタ 1
printer = nil // Printer の参照カウンタ 0 , クロージャの参照カウンタ 0

他と同様に弱参照を用いて防ぎます。

このケースの厄介な部分は、 func printName() の処理を見ただけだとクロージャがどう扱われているのかわからないという点です。 今回は func run(printClosure: @escaping () -> Void) の処理を見たときに、クロージャをプロパティへ代入していることがすぐにわかりますが、メソッド内の処理が複雑になってくると、クロージャがどう扱われるのかというのが把握しづらくなってしまいます。

@escaping

そこで @escaping というものがあります。

func run(printClosure: @escaping () -> Void) という部分で使われていますが、 これが付いていると、そのクロージャがどこかで強参照されるということを保証してくれます。そのため、このメソッドにクロージャを渡す際は、クロージャ内で扱うインスタンス[weak self] のように弱参照としておく必要があるということがすぐわかります。

まとめ

なんとなくわかった気がしたのですが、普段コードを書いていてもシュッと「これは循環参照するぞ」なんてわからなそうなので、迷ったらとりあえず weak をつけておけばいいのかなあという感じがしました。

追記

komaji504.hateblo.jp

上記のエントリで改めて @escaping 属性について書きましたが、 @escaping 属性がついていれば循環参照の可能性があるので弱参照にする、ついていなければ強参照にするで良さそうです。