【Swift】循環参照・weak/unownedはARCを知れば怖くない

Programming

Swiftを学んでいるとよく出てくる循環参照や、weak/unowned、弱参照・強参照。
普通にQiitaとかの解説記事を読んでいるだけではなかなか理解しにくいですが、これらの大元の原理となっているARCについて知っておけばするする頭に入ってくるかと思います。

ARCとは

Automatic Reference Counting(自動参照カウント)の略で、自動でSwiftのコンパイラがメモリ領域の管理を行ってくれる仕組みです。
他の言語ではGC(ガベージコレクション)によってメモリ管理を行っていることが多いかと思いますが、Swiftの場合このARCというもので管理を行います。
後述しますが循環参照問題は、このARCを使っていることにより生じてしまう問題です。そのためSwift以外の言語を使っている人はあまり聞いたことのない単語かと思います。

ARCはメモリ上のインスタンスやクロージャなどの参照型(Swiftでは関数は第1級オブジェクトなので参照型のオブジェクト扱いです)を、参照カウントを用いて管理します。

参照カウント

swiftではインスタンスは参照カウントで管理されています
参照の数が増えるたびに参照カウントが1ずつ増加し、減るごとに1ずつ現象、0になるとインスタンスを破棄します。

実例で見ていきましょう。


class Foo{} //クラスの定義 参照カウント:0
var foo: Foo? = Foo() //インスタンスを生成し、参照すると+1 参照カウント:1
var hoge = foo //インスタンスへの参照が増えるたびに+1 参照カウント:2
foo = nil //インスタンスを参照した変数にnilを入れて破棄すると-1 参照カウント:1
/*ここでインスタンス生成と同時に代入されたfooが破棄されるのにhogeの参照先であるFooインスタンスが残り続けるのは不思議に思えるが、そもそもswiftでは変数へのインスタンスの代入は実体をそのまま渡しているのではなく、メモリのどこかに作られた実体を参照するためのアドレス(ポインタ)を渡している(参照型)ので、変数fooにはFooインスタンスの実体は入っていない
*/
hoge = nil //参照カウントが0になると、インスタンスは破棄される  参照カウント:0

これらの参照カウントの増減を自動でやってくれる仕組みをARCといいます。
ちなみに昔(Objective-C時代)は、これらのメモリ管理をプログラマが手動で行っていたようです。泣きそうになりますね。

Swiftにおいてインスタンスは実体そのものを取り扱うのではなく、実体自体は他のところにあり、コード上で扱えるのはそのポインタのみです。
ならばどうやってコード上でインスタンスの生き死にを決めるのか?の答えが参照カウンタによる管理(0ならば死、1以上ならば生)です。

ここまでわかると、循環参照やweak/unownedの理解もしやすくなります。

循環参照とは

クラス間で相互に相手のインスタンスを参照し合うようなメソッドもしくはプロパティが存在する場合、それら全てのクラスのインスタンスへの参照を破棄しても、参照カウントは0にならないのでインスタンスがメモリ上に残り続けてしまう(メモリリーク)問題です。

swiftでは関数が第1級オブジェクトであることにより、クラス内に存在するクロージャがselfを強参照してしまうと、循環参照が起こえます。

実例を見てみましょう。


class Hoge{
    var fuga: Fuga?
    
    init() {
        print("Hoge爆誕!")
    }
    
    deinit {
        print("Hoge死亡...")
    }
}

class Fuga{
    var hoge: Hoge?
    
    init() {
        print("Fuga爆誕!")
    }
    
    deinit {
        print("Fuga死亡...")
    }
}

var hoge: Hoge?
var fuga: Fuga?

hoge = Hoge() //Hogeインスタンスの参照カウント:1
fuga = Fuga() //Fugaインスタンスの参照カウント:1

hoge!.fuga = fuga //Fugaインスタンスの参照カウント:2
fuga!.hoge = hoge //Hogeインスタンスの参照カウント:2

fuga = nil //Fugaインスタンスの参照カウント:1
hoge = nil //Hogeインスタンスの参照カウント:1

この結果出力されるのは


Hoge爆誕!
Fuga爆誕!

だけです。


Hoge死亡...
Fuga死亡...

が出力されません。
つまり、HogeインスタンスもFugaインスタンスも解放されません。ARCでは参照カウントが0にならないとオブジェクトの解放を行わないからです。

しかしhogeにもfugaにもnilを入れてしまっているので、もはやアクセスができなくなってしまい、誰も使えないのにメモリ上にインスタンスだけ残り続けてしまっている状態になります。この状態がメモリリークであり、このような問題を循環参照と言います。

これを解決するために出てくるのがweakやunownedです。
そのために、まず強参照・弱参照・非所有参照という概念について知る必要があります。

強参照・弱参照・非所有参照とは

強参照(strong参照)は参照するたびに参照カウントを1増やします。上のコードであったような普通の参照は、全てこの強参照です。

一方で弱参照(weak参照)と非所有参照(unowned参照)は、参照しても参照カウントを増やしません。weakもしくはunownedを変数宣言の前につけると、その変数の参照方式はweak参照・unowned参照になります。

実例をみてみましょう。


class Hoge{
    weak var fuga: Fuga?
    
    init() {
        print("Hoge爆誕!")
    }
    
    deinit {
        print("Hoge死亡...")
    }
}

class Fuga{
    var hoge: Hoge?
    
    init() {
        print("Fuga爆誕!")
    }
    
    deinit {
        print("Fuga死亡...")
    }
}

var hoge: Hoge?
var fuga: Fuga?

hoge = Hoge() //Hogeインスタンスの参照カウント:1
fuga = Fuga() //Fugaインスタンスの参照カウント:1

hoge!.fuga = fuga //Fugaインスタンスの参照カウント:1 weakによって弱参照になっているのでカウントは増えない
fuga!.hoge = hoge //Hogeインスタンスの参照カウント:2

fuga = nil //Fugaインスタンスの参照カウント:0 → 解放
//Fugaインスタンスの解放によりfuga!.hogeによる参照も消えるので、Hogeインスタンスの参照カウント:1
print(hoge.fuga)
hoge = nil //Hogeインスタンスの参照カウント:0 → 解放

とするとちゃんと


Hoge爆誕!
Fuga爆誕!
Fuga死亡...
nil
Hoge死亡...

と出力されます。

このようにweakプロパティをつけることにより参照カウントを増やすことなく参照できるため、fuga = nilによってしっかりとFugaインスタンスが破棄されます。

ちなみに、インスタンス生成時に代入される変数にweakをつけると、


class Hoge{
    
    init() {
        print("Hoge爆誕!")
    }
    
    deinit {
        print("Hoge死亡...")
    }
}

weak var hoge: Hoge?

hoge = Hoge() //Hogeインスタンスの参照カウント:0 → 解放

Hoge爆誕!
Hoge死亡...

爆誕から間も無く即死亡します。

インスタンスの生死はあくまでARCによって参照カウントで管理しているので、インスタンス生成時にweakやunownedをつけて参照するとインスタンスが生成された途端破棄されるという不思議な挙動を示します。

weakとunownedの違い

循環参照問題解決のためにはweak参照とunowned参照の2種類があります。
これらの大きな違いとしては、簡単にいうとweakがOptional、unownedがnon-Optionalです。

weak参照では、変数が参照した先のインスタンスが解放された時、その変数にはARCによってnilが代入されます。つまり、nilを許容するOptional型である必要があります。
一方unowned参照では、nilが代入されません。つまり、non-Optional型でいいわけです。

結果として、weak参照を使う変数の型はOptional型、unowned参照を使う変数の型はnon-Optional型になります。

例をあげると、


class Hoge{
    //weakによりfugaにnilが代入されうるのでOptional
    weak var fuga: Fuga?
    
    init() {
        print("Hoge爆誕!")
    }
    
    deinit {
        //self.fugaにはnilが入っている
        print(self.fuga)
        print("Hoge死亡...")
    }
}

class Fuga{
    var hoge: Hoge?
    
    init() {
        print("Fuga爆誕!")
    }
    
    deinit {
        print("Fuga死亡...")
    }
}

var hoge: Hoge?
var fuga: Fuga?

hoge = Hoge() //Hogeインスタンスの参照カウント:1
fuga = Fuga() //Fugaインスタンスの参照カウント:1

hoge!.fuga = fuga //Fugaインスタンスの参照カウント:1 weakによって弱参照になっているのでカウントは増えない
fuga!.hoge = hoge //Hogeインスタンスの参照カウント:2
weak var fuga2 = fuga
fuga = nil //Fugaインスタンスの参照カウント:0 → 解放
hoge = nil //Hogeインスタンスの参照カウント:0 → 解放

このようにweakを使うと、参照先のインスタンスが解放されても代わりにnilが入って問題なくアクセスできます。


Hoge爆誕!
Fuga爆誕!
Fuga死亡...
nil
Hoge死亡...

このようにprint(self.fuga)はnilが出力されます。

しかしunownedを使って


class Hoge{
    //unownedによりfugaにnilは代入されないのでnon-Optional
    unowned var fuga: Fuga

こうすると
fuga = nilによってFugaインスタンスが解放されますが、hoge!.fugaにはnilが代入されません。
したがって、hoge!.fugaは何も実体を持たないものになり、print(self.fuga)でアクセスエラーがおきます。

エラーが起きる可能性があるのならなんでunowned参照なんか使うのか?という話ですが、
unownedの場合はnon-Optionalなのでアンラップをしないですみ楽だからの一言に尽きます。

ただし、「アプリの動作全体を通じて参照先のインスタンスが解放される可能性が絶対にない」という確信を持てればの話です。
まだSwiftのOptional型に慣れないうちは下手にunownedを使わず、アンラップが手間でも全てweakを使っておいたほうが安全でしょう。

[weak self]、[unowned self]とは

クロージャの引数部分の前に宣言しておくことで、クロージャがselfを弱参照するようになります。


{ [weak self] a in ...}

循環参照対策に使われます。
その先selfにアクセスする時はアンラップしておきましょう。
guard let `self` = self else { return false }の一文で簡単にアンラップできます。
しかしこのようなバッククォートを使った書き方でのオプショナルバインディングは仕様上のバグらしいです。

まとめ

Swift特有のARCというガベージコレクションに似た仕組みは、インスタンスへの参照の数でインスタンスの生死を管理するようになっています。
参照の中にも強参照・weak参照・unowned参照とあり、下図のように特徴があります。

参照の種類 参照カウントの増加 参照先解放時のnil代入
強参照 あり あり
weak参照 なし あり
unowned参照 なし なし

オブジェクトの所有関係、nilの代入可能性を意識して使い分けていきましょう。

参考リンク

https://qiita.com/haranicle/items/184d5165353063fcc7c6
https://speakerdeck.com/oyuk/swiftdenande-weak-self-surufalseka?slide=19
https://qiita.com/rockname/items/b00d52c9bc49603f99a5

コメント

タイトルとURLをコピーしました