Swift言語では、Objective-Cと同様Automatic Reference Counting(ARC)を使用してメモリが管理されています。
大体の場合、何も考えなくてもARCが正しくメモリを解放してくれますが、場合によっては参照カウントを意識して、問題が起こらないように注意深くコーディングしないといけない場面もあります。
ここではARCの原理から、特有の問題点である循環参照によるメモリリークが発生する原因、その回避方法などを説明します。
ARCの原理
ARCの原理はシンプルです。例えば以下のような簡単なクラスを考えます。
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) init")
}
deinit {
print("\(name) deinit")
}
}
Personインスタンスを生成し、ref1、ref2、ref3に代入した後、順番にnilに設定していくと、最後の参照がnilに設定されたタイミングでdeinitが呼び出され、メモリが解放されることがわかります。
//リファレンスカウントの増減確認
var ref1: Person?
var ref2: Person?
var ref3: Person?
ref1 = Person(name: "田中") //カウント1
//=>田中 init
ref2 = ref1 //カウント2
ref3 = ref1 //カウント3
ref1 = nil //カウント2
ref2 = nil //カウント1
ref3 = nil //カウント0
//=>田中 deinit
Swiftでは通常変数への代入によって参照カウントが増えていきます。参照数が増える参照を「強参照」と呼びます。
なおOptional型は強参照(そして弱参照にも)関係ありませんのでご注意ください。
ARCの問題: 循環参照
インスタンスが互いに強参照している場合にARCでメモリが解放できない状況が発生します。例えば以下の2つのクラスを考えます。
//循環参照が発生する場合
class PersonWithStringApartment {
let name: String
init(name: String) { self.name = name }
var apartment: StrongApartment? //強参照
deinit { print("\(name) deinit") }
}
class StrongApartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: PersonWithStringApartment? //強参照
deinit { print("StrongApartment \(unit) deinit") }
}
それぞれ互いのインスタンスを強参照しているため、参照カウントが0にならずどちらのインスタンスも解放されない状態が発生します。
var tezuka: PersonWithStringApartment?
var unit101: StrongApartment?
tezuka = PersonWithStringApartment(name: "手塚治虫") //Personカウント1
unit101 = StrongApartment(unit: "101") //Apartmentカウント1
//循環参照状態を構築
tezuka!.apartment = unit101 //Apartmentカウント2
unit101!.tenant = tezuka //Personカウント2
//解放したいが…
tezuka = nil //Personカウント1
unit101 = nil //Apartmentカウント1
//=>カウントが0にならないためどちらのdeinitも呼ばれない
循環参照をweak参照で解決する
循環参照を解消する方法はもちろん準備されています。その方法の一つ目はweak参照を使用することです。WeakApartmentクラスは「weak var tenant」と頭にweakをつけてインスタンスを参照していることに注目してください。
weak参照は参照カウントが増えない参照なのです。
class PersonWithWeakApartment {
let name: String
init(name: String) { self.name = name }
var apartment: WeakApartment?
deinit { print("\(name) is being deinitialized") }
}
class WeakApartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: PersonWithWeakApartment?
deinit { print("Apartment \(unit) deinit") }
func tenant_name() -> String? {return tenant?.name}
}
WeakApartmentからはweak参照が使われているのでうまく解放することができます。ただしweak参照はOptionalなので使用する際にunwrapする必要があります。
var fujiko: PersonWithWeakApartment?
var unit102: WeakApartment?
fujiko = PersonWithWeakApartment(name: "藤子不二雄") //Personカウント1
unit102 = WeakApartment(unit: "102") //Apartmentカウント1
//循環参照構築
fujiko!.apartment = unit102 //Apartmentカウント2
unit102!.tenant = fujiko //Personカウント1
//解放処理
fujiko = nil //Personカウント0,Apartmentカウント1 住人は弱参照されているだけなので単独解放可能
unit102!.tenant //=>nil weak参照の場合参照先オブジェクトが解放されるとnilになる
unit102 = nil //Apartmentカウント0 住人からの参照がなくなっているので部屋も解放可能
//=>どちらのdeinitも呼ばれる
循環参照をunowned参照で解決する
weakと似たunownedを使うこともできます。
class PersonWithUnownedApartment {
let name: String
init(name: String) { self.name = name }
var apartment: UnownedApartment?
deinit { print("\(name) is being deinitialized") }
}
class UnownedApartment {
let unit: String
unowned var tenant: PersonWithUnownedApartment
init(unit: String, tenant: PersonWithUnownedApartment) {
self.unit = unit
self.tenant = tenant
}
deinit { print("Apartment \(unit) deinit") }
func test() {tenant}
func tenant_name() -> String? {return tenant.name}
}
unownedは非Optionalである代わりにインスタンス解放時にnilになりません。絶対にnilにならない場合に使用する必要があります。
var akatsuka: PersonWithUnownedApartment?
var unit103: UnownedApartment?
akatsuka = PersonWithUnownedApartment(name: "赤塚不二夫") //Personカウント1
unit103 = UnownedApartment(unit: "103", tenant: akatsuka!) //Apartmentカウント1
//循環参照構築
akatsuka!.apartment = unit103 //Apartmentカウント2
//解放処理
akatsuka = nil //Personカウント0,Apartmentカウント1 住人は弱参照されているだけなので単独解放可能
unit103!.tenant //=>unownedの場合nilにならない
unit103 = nil //Apartmentカウント0 住人からの参照がなくなっているので部屋も解放可能
//=>どちらのdeinitも呼ばれる
ハマりやすいClosureからの循環参照
循環参照のバリエーションとしてClosureを使用する際、無意識のうちに循環参照状態になってしまうというものがあります。
インスタンスのプロパティとしてClosureを強参照し、Closureの処理内からselfでインスタンスを強参照すると循環参照が発生します。
//Closureを使って循環参照が発生する場合
class ClosureDemo {
var myClosure: (() -> Void)? //Closureを強参照
var name = "test"
init() {
myClosure = {
self.name //selfを強参照
}
}
deinit {
print("ClosureDemo deinit")
}
}
var closureDemo: ClosureDemo? = ClosureDemo()
closureDemo = nil//=>解放されない
この場合も通常の循環参照を解消する場合と同様に、weak/unowned参照を使って問題を解決することができます。
//Closureの循環参照をweakで解決
class WeakClosureDemo {
var myClosure: (() -> Void)? //Closureを強参照
var name = "test"
init() {
myClosure = {
[weak self] () in
self?.name //selfを弱参照(nilになる可能性があるので"?"が必要)
}
}
deinit {
print("WeakClosureDemo deinit")
}
}
var weakClosureDemo: WeakClosureDemo? = WeakClosureDemo()
weakClosureDemo = nil
//=>WeakClosureDemo deinit
Closureがプロパティではない場合は問題ありません。
//Closureがローカルなら問題ない
class LocalClosureDemo {
var name = "test"
init() {
let myClosure = {
[weak self] () in
self?.name //selfを弱参照(nilになる可能性があるので"?"が必要)
}
myClosure()
}
deinit {
print("LocalClosureDemo deinit")
}
}
var localClosureDemo: LocalClosureDemo? = LocalClosureDemo()
localClosureDemo = nil
//=>WeakClosureDemo deinit
UIAlertControllerの表示処理って?
Closureを積極的に使うことはあまり無いとしても、UIKitを使っていると至る所で気にしないといけない部分がでてきます。例えばUIAlertControllerを使ってアラートを表示し、ボタンが押された場合の処理を行う箇所もClosureです。
class AlertViewController: UIViewController {
var name = "デモ画面です"
// var alert: UIAlertController! //ここに保持してしまうとまずい…
// var cancelAction: UIAlertAction!
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Alertのデモ"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
deinit {
print("AlertViewController deinit")
}
@IBAction func buttonTapped(sender: AnyObject) {
let alert = UIAlertController(title: "アラート", message: "アラートのメッセージです", preferredStyle: UIAlertControllerStyle.Alert)
let cancelAction = UIAlertAction(title: "キャンセル", style: UIAlertActionStyle.Cancel, handler: {
(action:UIAlertAction!) -> Void in
print("キャンセル" + self.name) //強参照
})
alert.addAction(cancelAction)
presentViewController(alert, animated: true, completion: nil)
}
}
上記の例の場合、alertはローカル変数として保持されているため循環参照は発生しません。alertをプロパティとして保持しない場合はweak selfでキャプチャしなおす必要はないのです。
ARCのまとめ
循環参照が発生するとメモリリークが発生する可能性があるので要注意です。ただしここで問題になるのはあくまで循環参照が発生する場合なので、例えばViewController間で引数オブジェクトを引き回す場合は問題ありません(循環参照しているわけではないので)。
またClosure内でselfを使う場合循環参照が発生しがちなので注意が必要です。同じようなパターンとしてdelegateを使って処理を以上するパターンもあります。