ARC
概要
ARC とは
ARC (Automatic Reference Counting) とは、自動的なメモリ管理を提供する コンパイラの機能で、コンパイル時に、オブジェクトが必要最低限の期間生存するのに必要なコードを追加することで実現される。
クラスのインスタンスを生成すると、ARC がメモリのチャンクを割り当て、インスタンスの型情報, 関連する Stored property の情報等を格納する。そして、ARC によってメモリが解放されると、インスタンスのプロパティ、メソッドが参照できなくなり、アクセスしようとするとアプリがクラッシュする。 ARC では、インスタンスがまだ必要なのに削除されることがないように、そのインスタンスに対する 強参照 (strong reference) の数をカウントする。これが 1 以上なら解放せず、0 になった時点で解放する。
MRC について
Xcode 4.2 より以前は MRC (Manual Reference Counting) という仕組みが使われていた。この仕組みでは、インスタンスを作成したら、その保持 (retain) と解放 (release) の責務を、開発者自身が負う必要があったが、ARC では、そのようなメモリ管理の殆どが自動で行われる。
強参照を試す
code:arc.swift
class User {
let name: String
init(name: String) {
self.name = name
print("\(name) is initialized")
}
deinit {
print("\(name) is deinitialized")
}
}
// 3つの変数に代入することで、Userのインスタンスを強参照する変数が3つになる
var user: User? = User(name: "tasuwo")
var user2: User? = user
var user3: User? = user
// 2つにnilをセットすると強参照は2つになるが、user3がまだ参照しているので、インスタンスは解放されない
user = nil
user2 = nil
// 元々の変数userにはnilが格納されたが、まだインスタンスが解放されていないので、アクセスできる
print(user3?.name)
// この時点でdeinitが呼ばれる
user3 = nil
print(user3?.name)
code:shell
$ xcrun swiftc arc.swift
$ ./arc
tasuwo is initialized
Optional("tasuwo")
tasuwo is deinitialized
nil
一部を weak に書き換える。すると、user に nil が代入された時点で deinit が呼ばれる。
code:arc.swift
var user: User? = User(name: "tasuwo")
weak var user2: User? = user // weak に書き換え
weak var user3: User? = user // weak に書き換え
code:shell
$ xcrun swiftc arc.swift
$ ./arc
tasuwo is initialized
tasuwo is deinitialized
nil
nil
メモリリーク
とは?
メモリリーク とは、メモリの一部が占有された状態のまま二度と解放されず、再利用できない状態に陥ること
問題点
メモリフットプリントの増加: オブジェクトがリリースされない場合、その分メモリ使用量が増加する。さらに、そのようなオブジェクトを作成する処理が複数回呼ばれると、その回数分メモリ使用量が増加し、最終的にはメモリワーニングの状態から、アプリのクラッシュへと至る
予期しない副作用: 例えば、通知に応じて様々な処理 - 例えば、ビデオの再生やDBへの格納等 - を行うオブジェクトが残り続けた場合、それらの処理が意図せず動き続けるし、場合によっては重複して動いてしまう状況に陥る
原因
一番多いのは retain cycle (strong reference cycle, 相互強参照) によるもの。インスタンスが互いに強参照しあっていると、インスタンスが解放されない。retain という用語は MRC 時代の名残らしい。
Retain cycle の解消
これを避けるために、クラス間の関連に強参照の代わりに weak reference や unowned reference を利用する。これらは、インスタンスを強参照なしで保持することができる。これらはどちらも、保持対象のインスタンスを強参照しない、という点では同一で、参照元/参照先間の生存期間の違い、もっというと、参照先のインスタンスが nullable かそうでないか?で使い分けることができる。
weak reference は、参照先のインスタンスの生存期間の方が参照元より短い時に推奨される。参照元より参照先の方が生存期間が短いということは、参照先が存在しない可能性もあるため、値が nil になり得る
unowned reference は、参照先のインスタンスの生存期間の方が参照元より長い、あるいは同一の時に推奨される。参照元より参照先の方が生存期間が長いということは、参照先は常に存在するはずなので、値は nil になり得ない
メモリリークを観測する
LifetimeTracker
割愛
Xcode Visual Memory Debugger
Xcode 8 から導入された機能に、Debug Memory Graph がある。これは、その名の通りメモリの状態をグラフとして可視化するツールで、メモリリークが発生しているときにどこがメモリリークしているか?を調査できる。 例えば、適当な iOS プロジェクトを立ち上げて、ViewController に以下のように定義する。
code:ViewController.swift
import UIKit
class ViewController: UIViewController {
class Book {
self.pages = pages
_ = self.pages.map { $0.book = self }
}
}
class Page {
var book: Book?
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
上記のコードでは、Book が Page を強参照しており、さらにその逆も行なっているため、メモリリークを引き起こす。実際に Page と Book のインスタンスを各々作成し、互いに強参照させるために代入する。この時、Book のインスタンスを ViewController に保持せずに捨てると、メモリリークとなる (ViewController に Book のインスタンスを保持させると、インスタンスへの参照自体は残るので、メモリリークの発生として Debug Memory Graph 上で検知されない)。
上記を実行すると、Debug Memory Graph 上で ! マークが付与され、メモリリークが発生している場所がわかるようになる。
https://gyazo.com/b41c5958db36167dbb7e18c804ac2700
コードの一部を書き換えてみる。
code:ViewController.swift
class Page {
weak var book: Book? // weak にする
}
再度実行すると、Book と Page のインスタンスがどちらも破棄され、メモリリークが解消されたことがわかる。
https://gyazo.com/40dea6c4caf89759e620e5e6aeda3ea1
メモリリークをテストする
SpecLeaks という、メモリリークしているかどうか?をテスト可能にするライブラリがあるようだ。
あとで読む