【Swift】weakの役割とは?循環参照の意味とARCについて
この記事からわかること
- Swiftのメモリ管理の仕組み
- ARC(Automatic Reference Countingとは?
- 循環参照が発生する原因
- weakキーワードの使い方
- 強参照と弱参照、非所有参照の違い
- unownedキーワードの使い方
- [weak self]の意味
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
この記事はweakとはどのような役割があり、どのように使うのかを以下の公式リファレンスを要約し自分なりにまとめたものになっています。勘違いや至らぬ点がありましたら教えていただけると嬉しいです。
公式リファレンス:Automatic Reference Counting
結論
weakは循環参照を防ぐためのキーワード
Swiftのメモリの仕組み:ARC
weakの役割を理解するためにはSwiftのメモリ(容量)管理を理解する必要があります。SwiftのメモリはARC(Automatic Reference Counting)と呼ばれる仕組みで管理されており、内部的に自動でメモリの確保と解放を行なってくれています。
ARCのメモリ管理で肝となるのが参照です。ARCではメモリを解放するタイミングはガーベジコレクションの要領でそのメモリへの参照が0になったタイミングで行うようです。
Swiftのクラスは参照型でデータを保持しているのでこの参照カウントはクラスに対してのみ行われます。構造体や列挙型は値型なのでコピーした際は値がコピーされるのでメモリに対しての参照を増えません。
Swiftのクラス自体についてや参照型と値型の違いについては以下の記事を参考にしてください。
メモリを確保するタイミング
ARCがメモリを確保するのはクラスのインスタンスを生成した時です。そこからプロパティや変数に格納されるとそのメモリ(インスタンス)の参照という形で紐付けがカウントされます。この参照を強参照と呼びます。
class Person {
let name: String
init(name: String) {
self.name = name
}
}
// メモリを確保と変数への参照がカウント
var person1:Person? = Person(name: "ame")
変数に格納されたインスタンスを別の変数にコピーした場合は、参照カウントは2になります。
// クラスをコピー
var person1Copy:Person? = person1
メモリを解放するタイミング
メモリを解放するタイミングはインスタンスへの参照が0になった時でした。なのでインスタンスを格納した変数にnil
などが格納されると参照が解除され、他に参照がなければメモリが解放されます。コピーを作成していれば2つとも紐付けを切ることで解放されます。
person1 = nil // メモリはまだ解放されない
person1Copy = nil // ここで参照が0になり解放
イニシャライザとデイニシャライザ
クラスではクラスのインスタンス化時と破棄する時をイニシャライザとデイニシャライザで取得することができるので、変数に格納した時のメモリの確保と解放のタイミングを確認することが可能です。
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) が生成されました")
}
deinit {
print("\(name) が削除されました")
}
}
上記の例ではインスタンス化したタイミングにイニシャライザが呼ばれ、nilを格納したタイミングでデイニシャライザが呼ばれているのが確認できます。
var person1:Person? = Person(name: "ame")
print("-----")
person1 = nil
// ameが生成されました
// -----
// ameが削除されました
ですが変数との紐付けを解除してもメモリが解放されない場合があります。それが「循環参照」です。
循環参照とは?
循環参照とは2つのインスタンス間でメモリの参照が循環に行われメモリの参照が0にならず解放されなくなってしまうことです。
循環参照が良くない理由は不要なはずのメモリが解放されない点です。本来もう使用することはない(保存する必要のない)データがメモリ内に永続的に残ってしまいます。
循環参照が起きてしまう例として以下のようなインスタンス同士をプロパティに紐付ける場合を見てみます。
class Person {
let name: String
init(name: String) { self.name = name }
var lang: Language?
deinit { print("\(name) が削除されました") }
}
class Language {
let name: String
init(name: String) { self.name = name }
var owner: Person?
deinit { print("Language: \(name) が削除されました") }
}
定義は上記のようになります。それぞれのインスタンスを各プロパティに紐付けます。
var ame:Person? = Person(name: "ame")
var swift:Language? = Language(name: "Swift")
ame!.lang = swift
swift!.owner = ame
この場合変数ameにnilを格納(Personインスタンスの参照を解放)してもPersonインスタンスはLanguageインスタンスのプロパティに紐づいているため参照は0にはなりません。
ame = nil
// デイニシャライザが呼ばれない
このまま変数swiftにnilを格納するとPersonインスタンスとLanguageインスタンス同士の参照が残ったまま変数との参照は断ち切られるので解除方法が無くなったままメモリに残ってしまうこと(循環参照)になります。
swift = nil
// デイニシャライザが呼ばれない
weakキーワード
この循環参照を防ぐ目的で使用されるのがweakキーワードです。このweakキーワードをプロパティ宣言の前に付与することで参照の強さを変更することができます。
例えば以下のような定義の場合にLanguageクラスのプロパティにweakキーワードを付与したとします。
class Person {
let name: String
init(name: String) { self.name = name }
var lang: Language?
deinit { print("\(name) が削除されました") }
}
class Language {
let name: String
init(name: String) { self.name = name }
weak var owner: Person?
deinit { print("Language: \(name) が削除されました") }
}
本来で有れば変数ameにnilを格納してもPersonインスタンスとLanguageインスタンスの参照は保持されたままですがweakキーワードがついている場合はこの時点でPersonインスタンスとLanguageインスタンスの参照も解放され、デイニシャライザが起動していることが確認できます。
var ame:Person? = Person(name: "ame")
var swift:Language? = Language(name: "Swift")
ame!.lang = swift
swift!.owner = ame
ame = nil
print("------")
swift = nil
// ame が削除されました
// ------
// Language: Swift が削除されました
強参照と弱参照、非所有参照
ARCではインスタンスとデータの参照は基本的に「強参照」と呼ばれる強い参照で紐付けられ参照カウントが増やされます。そしてそのデータに対して強参照が残っている(カウントが0でない)限りメモリが解放されることはなく、また別インスタンスからの強参照により循環参照などが発生する原因になります。
これを防ぐために参照の強さを変更できるのがweak
やunowned
などのキーワードです。参照の強さを変更することで参照カウントを増やすことなく参照することができる
ようになります。
weak:弱参照
weak
キーワードを付与した場合は弱参照になります。インスタンスに対しての強い保持がなくなり、参照先のインスタンスの破棄を妨げることはありません。
参照先のインスタンスが解放された場合は自動的にnil
が格納されるようになっているので定義時にOptional型
である必要があります。
unowned:非所有参照
unowned
キーワードを付与した場合は非所有参照になります。非所有参照も弱参照と同じくインスタンスに対しての強い保持がないので、参照先のインスタンスの破棄を妨げることはありません。
非所有参照と弱参照が大きく異なるのは解放時にnilが自動で格納されない点です。unowned
キーワードを使用するタイミングは参照するインスタンスの有効期間が同じか、長い場合に使用されます。
つまりAインスタンスがなくなった時にBインスタンスの存在理由も抹消されるような関係性の場合に使用します。顧客とクレジットカードインスタンスの関係性がわかりやすい例です。
クロージャーに記述する[weak self]
循環参照自体は参照型のオブジェクト間で発生する問題でした。これはクラス同士だけでなくクロージャーを使用した場合にも関係してきます。
クロージャーで関係してくるのはself
を使って自身を参照する時です。以下のようにクロージャー内からself
を参照する時も強参照になってしまうので明示的に[weak self]
を付与することで弱参照にしています。
class Hoge {
private var closure: (() -> Void)?
private var count = 0
init() {
closure = createClosure()
}
func createClosure() -> (() -> Void) {
return { [weak self] in self.count += 1 }
}
}
var h: Hoge? = Hoge()
h = nil
ちなみに[weak self]
部分はキャプチャリストと呼び、クロージャー内で使用する変数のキャプチャを明示的に制御する役割があります。[self]
とした場合は明示的に強参照を指定したことになり、普段は省略されています。
おすすめ記事:【Swift】クロージャとは?関数との違いとキャプチャの意味
[weak self]
については以下記事を参考にさせていただきました。
参考文献:Swiftでなんで[weak self]するのか?
まだまだ勉強中ですので至らぬ点や間違っている点がありましたら教えていただけると嬉しいです。
ご覧いただきありがとうございました。