普段コードを書いている時に
Swiftが内部で
どのようにオブジェクトを管理しているのかについて
考えることはあまり多くないかもしれません。
しかし
非同期処理を扱う場合など
DispatchQueue.main.async { [weak self] in
...
}
などのように
weak
といった
キーワードを使用することは多くあると思います。
これは「弱参照」と呼ばれ
直接の参照(強参照)を持たないように
Swiftの内部に指示を出して
循環参照を起こさないための仕組みです。
こういった適切なメモリ管理を行わないと
メモリが解放されないことでランダムにクラッシュするなど
原因がわかりづらい不具合を発生させる可能性があります。
そこで今回は
weak
などを使用することで
Swiftの内部で何が起こるのかを見ていき
weak
やunowned
の使い方や
オブジェクトのライフサイクルについて
学んでみたいと思います。
メモリの3つ仮想的な領域
メモリ自体はただのバイトの配列ですが
プログラミングという観点で考えると
主に3つの領域に分かれます。
- スタック領域 全てのローカル変数を保管している場所
- グローバルデータ 静的な変数や定数や型のメタ情報を保管している場所
- ヒープ領域 オブジェクト※を保管している場所
※
実行時にメモリを割り当てられ
ある時点で解放される「寿命」を持つものを指します。
Swiftでは主に参照型(reference type)を意味します。
ARC(Automatic Reference Counting)
メモリ管理には「オーナーシップ(所有権の帰属)」という概念が大切です。
これは、あるオブジェクトを誰が解放する責務を持っているかということを意味します。
詳細に関しては下記のOwnershipManifestに記載されています。
https://github.com/apple/swift/blob/master/docs/OwnershipManifesto.md
このオーナーシップを管理する仕組みとして
SwiftはARCを使用しています。
ARCはオブジェクトの参照されている数(参照カウント)を保持しておき
カウントがゼロになるとメモリから自動で解放される仕組みです。
2つのレベルの参照(強参照と弱参照)
Swiftでは参照に2つのレベルがあります。
それが冒頭でも登場した
強参照(strong reference)
と
弱参照(weak reference)
です。
さらに
弱参照の派生として**非参照(unowned reference)**があります。
強参照(strong reference)
Swiftでは
強参照がある限り
オブジェクトは生存し続けることができ
逆になくなるとメモリから解放されます。
SwiftではJavaのガベージコレクションのように
自動でメモリを解放する仕組みは持っていないため
循環参照を引き起こす可能性があります。
たとえば
オブジェクトAとオブジェクトBが
お互いにオブジェクトへの参照を持っている状態です。
これを防ぐためにはweak
などの記述が必要になってきます。
弱参照(weak reference)
弱参照を使用することで
循環参照を断ち切ることができますが
弱参照を持っていたとしても
強参照がなくなればオブジェクトはメモリから解放されてしまいます。
そして
参照しているオブジェクトを使おうとしてもnil
になっています。
そのためweak
を付けたオブジェクトはOptional
として扱います。
非参照(unowned reference)
弱参照とほぼ同じですが
参照しているオブジェクトがnil
の状態で使おうとすると
assertionエラーでプログラムはクラッシュします。
そのため、unowned
は、参照がなくならないと想定しているのに
予期せず参照が解放されてしまっている不具合を発見するのに役立ちます。
unowned
を使用する基準としては
参照するオブジェクトと参照されるオブジェクトの寿命が同じような時です。
たとえばクラスの中でlazy
を付けた変数を定義したとします。
class ViewController: UIViewController {
lazy var label: UILabel = { [unowned self] in
self.someSetup()
...
}()
}
これはViewController
クラスの変数label
が
self
(ここではViewController
)を参照しています。
これはクラスオブジェクトが解放されるタイミングで
label
変数も解放されるので
self
の参照がなくなることはありません。
参照については下記のドキュメントに詳細が記載されています。
https://github.com/swiftlang/swift/blob/main/docs/WeakReferences.md
Swift Runtime
ARCはSwift Runtimeというライブラリで実装されています。
他にもSwift Runtimeでは
実行時にジェネリクスやプロトコルを具体的な型に解決する
などの重要な機能を有しています。
全てのオブジェクトは
**HeapObject
**というstruct
で表現されます。
HeapObjectはオブジェクトの型のメタ情報と参照カウント(RefCount)を持っています。
RefCountにはstrong
とweak
とunowned
用の3種類があります。
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h
SwiftのコンパイラはSIL生成段階(SIL Generation)※で
swift_retain()
swift_release()
というメソッドを適切な場所に差込みます。
これによってHeapObject
の作成や解放がされます。
※
SIL生成段階(SIL Generation)はSwiftのコンパイル時の一つのフェーズです。
下記のドキュメントに詳細が記載されています。
https://swift.org/compiler-stdlib/#compiler-architecture
Side Table
全てのオブジェクトは
弱参照(weak reference)用のRefCountを持てるものの
実際には多くのオブジェクトで弱参照を使いません。
そこで、弱参照用のRefCountにメモリを割り当てても無駄になることが多いため
弱参照の情報はSide Table※という別の場所に保管され
本当に必要になった時にメモリが割り当てられるようになっています。
※
正式にはHeapObjectSideTableEntry
です。
内部ではオブジェクトのポインタとRefCountを持っています。
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h#L1257
弱参照が示すメモリアドレスは参照したいオブジェクトではなく
このSide Tableを示しています。
こうすることで
無駄にメモリを消費することがないことに加え、
直接オブジェクトを参照していないため
オブジェクトの解放と弱参照の参照のタイミングが競合することなく
弱参照を取り除くことができます。
オブジェクトのライフサイクル
下記のコメントを参考にすると
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h#L112
Swiftのオブジェクトは3つの参照の保持の仕方によって
状態を5つに分けることができます。
- LIVE
- DEINITING
- DEINITED
- FREED
- DEAD
簡単に図にすると下記のように状態が変化していきます。
次にそれぞれの状態について
見ていきます。
各状態で弱参照(Side Table)があるかないかで
挙動や次に遷移する状態が変化していきます。
LIVE without side table
オブジェクト
生存している
参照カウント
強参照1 非参照1 弱参照1で初期化される。
Side Tableと弱参照カウントのメモリ割り当て
なし
強参照変数の操作
正常に機能する
非参照変数の操作
正常に機能する
弱参照変数を使用した時の挙動
起こり得ない
弱参照変数へのオブジェクトの代入
Side Tableを追加する
LIVE with Side Table状態になる
次の状態への遷移
参照がゼロになった時
deinitが呼ばれDEINITINGの状態になる
LIVE with side table
弱参照変数の操作
正常に機能する
それ以外は
LIVE without side tableと同じ
DEINITING without side table
オブジェクト
deinit()
を実行中。
強参照変数の操作
何も起こらない。
非参照変数を使用した時の挙動
swift_abortRetainUnowned()
で処理が中断されます。
https://github.com/apple/swift/blob/ebcbaca9681816b9ebaa7ba31ef97729e707db93/include/swift/Runtime/Debug.h#L122
非参照変数へオブジェクトを代入した時の挙動
正常に機能する
弱参照変数を使用した時の挙動
起こり得ない
弱参照変数へオブジェクトを代入した時の挙動
nilが代入される
次の状態への遷移
参照がゼロになった時の挙動
deinit()
が完了し
swift_deallocObject()
というメソッドが呼ばれる。
canBeFreedNow()
で弱参照または非参照があるかどうかをチェックする。
canBeFreedNow
がtrueの場合
オブジェクトは解放されてDEINITEDの状態になる。
DEINITING with side table
弱参照変数を使用した時の挙動
nil
を返却する
弱参照変数へオブジェクトを代入した時の挙動
nil
が代入される
canBeFreedNow()
は常にfalseになり
そのままDEAD状態にはならない。
その他はDEINITING without side tableと同じ。
DEINITED without side table
オブジェクト
deinit()
は完了しているが
非参照は存在している。
強参照変数の操作
起こり得ない
非参照変数を使用した時の挙動
swift_abortRetainUnowned()
の中でロードを停止している
非参照変数へオブジェクトを代入した時の挙動
起こり得ない
弱参照変数の操作
起こり得ない。
次の状態への遷移
非参照カウントがゼロになった時
オブジェクトは解放されてDEAD状態になる
DEINITED with side table
弱参照変数を使用した時の挙動
nilが返却される
弱参照変数へオブジェクトを代入した時の挙動
起こり得ない
次の状態への遷移
非参照カウントがゼロになった時
オブジェクトは解放されて弱参照カウントが減り
オブジェクトはFREED状態になる
他はDEINITED without side tableと同じ
FREED without side table
起こり得ない
FREED with side table
オブジェクト
オブジェクトは解放されているが
弱参照がSide Tableに残っている
強参照の操作
起こり得ない
非参照の操作
起こり得ない
弱参照変数を使用した時の挙動
nil
が返却される
弱参照変数へオブジェクトを代入した時の挙動
起こり得ない
次の状態への遷移
弱参照カウントがゼロになった時
Side Tableオブジェクトは解放され
オブジェクトはDEAD状態になる
DEAD
オブジェクトもSide Tableもなくなっている
まとめ
Swiftの内部の仕組みから
メモリ管理について見ていきました。
普段はあまり意識していませんでしたが
こういう知識を知っていることで
原因がわからない不具合などの解決にも
役に立つことがあるかもしれません。
また
XcodeにはMemoryDebuggerもあり
そのグラフを理解するのに役に立つかもしれません💡
もし何か間違いなどございましたら
ご指摘頂けましたらうれしいです🙇🏻♂️
参考記事