はじめに
こんにちは。haranicleです。
2015年。エンジニアにも筋肉が求められる時代になりました。
今回は、筋肉Swiftプログラマーになりたい皆様といっしょに、
Swiftの基礎であるARCについてトレーニングしていきましょう。
がんばりましょう。💪💪💪
前提
Swift2.1についての記載しています。
ARCの基礎
ARCとは
プログラマーが意識していなくても、Swift(のコンパイラ)がARC(Automatic Reference Counting)という仕組みを使ってメモリ領域管理をしてくれます。
もやしプログラマーが飯を食っていけるのもARCのおかげです。
ARCは、基本的に以下のルールに基づいてメモリ領域を管理します。
- 参照型(クラスのインスタンスとクロージャなど)のみを対象にする
- インスタンスを生成した時にメモリ空間を確保する
- インスタンスが 必要 な間は、メモリ空間を確保し続ける
- インスタンスが 不要 になった時に、メモリ空間を開放する
参照カウント
ARCは参照カウントという仕組みを利用して、インスタンスが 必要 or 不要 を判定しています。
参照カウントは以下のように定義されています。
- インスタンスごとに割り振られる値
- インスタンスを参照(*1)しているプロパティ、定数、変数の個数
Objective-CでARCをOFFにすると、retainCount
というメソッドで参照カウントを取得できます。
*1 後述しますが、"参照"はstrong参照と呼ばれるもののみが対象です。
ARCの挙動
- 新しいインスタンスを作るとARCがメモリ領域を確保する
- メモリは以下を保持する
- インスタンスの型
- ストアドプロパティ(*2)
- メモリは以下を保持する
- インスタンスの参照カウントが0になったら、そのインスタンスを記録しているメモリ領域を開放する
- 開放したメモリ領域は、他の目的に使用することができる
*2 ストアドプロパティ、コンピューテッドプロパティについては、以下を参照してください
The Swift Programming Language (Swift 2.1): Properties
ARCの挙動サンプルコード
The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person? // == nil (Optionalなのでnilで初期化される)
var reference2: Person? // == nil
var reference3: Person? // == nil
reference1 = Person(name: "John Appleseed") // John Appleseedが作られる
// "John Appleseed is being initialized"とprintされる
// reference1がJohn Appleseedへの参照を持つためJohn Appleseedの参照カウントが1になる
// John Appleseedは、参照カウントが0でないので開放されない
reference2 = reference1 // reference2はJohn Appleseed を参照している(参照カウント2)
reference3 = reference1 // reference3はJohn Appleseed を参照している(参照カウント3)
reference1 = nil // reference1は何も参照しない(参照カウント2)
reference2 = Person(name: "haranicle") // reference2 が他のインスタンスを参照するようになった(参照カウント1)
// John Appleseedは、参照カウントが1以上なのでdeallocされない
reference3 = nil // reference3は何も参照しない(参照カウント0)
// "John Appleseed is being deinitialized"とprintされる
// John Appleseedの参照カウントが0になったので開放された
do {
let reference4:Person = Person(name: "Kazushi Hara") // reference4はKazushi Haraを参照している(参照カウント1)
// ここでスコープ抜ける
}
// "Kazushi Hara is being deinitialized"とprintされる
// reference4がなくなるのでKazushi Haraの参照カウント0になり、開放された
ARCの罠 "循環参照"
循環参照とは
- 複数のインスタンスが互いに参照をもつとき、参照カウントが0にならず、インスタンスが開放されない 循環参照 という現象を起こすことがあります。
- 循環参照は、プログラマーの筋肉で解決してあげる必要があります。
循環参照のサンプルコード
The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed") // johnの参照カウント1
unit4A = Apartment(unit: "4A") // unit4Aの参照カウント1
john!.apartment = unit4A // unit4Aの参照カウント2 (*事件現場A)
unit4A!.tenant = john // johnの参照カウント2 (*事件現場B)
john = nil // johnの参照カウント1
unit4A = nil // unit4Aの参照カウント1
// 何もprintされない
// _人人人人人人人人人人人人人人人人人人_
// > johnとunit4Aが開放されない!! <
//  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
循環参照の解決方法
strong参照
今まで、何度も"参照"というワードを使ってきましたが、厳密にはstrong参照と呼ばれるものを指しています。
strong参照は、以下のように記述します。
strong var john = Person(name: "John Appleseed")
// Swiftではデフォルトでstrong参照になるため、"strong"は省略できる
var haranicle = Person(name: "Kazushi Hara")
前述しましたが、strong参照するとインスタンスの参照カウントが+1されます。
循環参照のサンプルコードの事件現場Aか事件現場Bで参照カウントが増えなければ、今回のような悲惨な結果には、ならなかったはずです。
Swiftは、参照カウントが増えない(参照カウントのカウント対象にならない)参照の方法を2つ提供しています。
weak参照
weak参照は以下のような特徴があります。
- 参照しても、参照カウントが増えない
-
参照先がnilになることがある
- Optional型
- 定数で使用することはできない
- 使おうとしたらXcodeで以下のようなエラーが出ます
'weak'must be a mutable variable, because it may change at runtime
- 使おうとしたらXcodeで以下のようなエラーが出ます
使用するパターン
以下のようなパターンで使います。
- AはBの参照をもつことができる
- BはAの参照をもつことができる
weak参照のサンプルコード
The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person? // weakになっている!
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed") // johnの参照カウント1
unit4A = Apartment(unit: "4A") // unit4Aの参照カウント1
john!.apartment = unit4A // unit4Aの参照カウント2
unit4A!.tenant = john // johnの参照カウント1
john = nil // johnの参照カウント0
// John Appleseed is being deinitializedとprintされる
unit4A = nil // unit4Aの参照カウント0
// Apartment 4A is being deinitializedとprintされる
unowned参照
unowned参照は以下のような特徴があります。
- 参照しても、参照カウントが増えない
-
参照先がnilになることはない
- Optional型ではない
- 参照先が解放済みのとき、unowned参照のプロパティ、変数にアクセスするとランタイムエラーでアプリがクラッシュする
- 絶対に参照先が開放されていない自身があるときはunownedを使用する
- 定数で使用することはできない
- 使おうとしたらXcodeで以下のようなエラーが出ます
'weak'must be a mutable variable, because it may change at runtime
- 使おうとしたらXcodeで以下のようなエラーが出ます
使用するパターン
以下のようなパターンで使います。
- AはBの参照をもつことができる
- BはAの参照が必要
unowned参照のサンプルコード
The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized");}
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // unownedになっている!
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer // initでcustomerをセットするので、CreditCardのインスタンスが生きている間にcustomerがnilになることはない
}
deinit { print("Card #\(number) is being deinitialized");}
}
var john: Customer? = Customer(name: "John Appleseed") // johnの参照カウントは1
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!) // johnの参照カウントは1, cardの参照カウントは1
john = nil // johnの参照カウントは0, cardの参照カウントは0
// John Appleseed is being deinitializedとprintされる
// Card #1234567890123456 is being deinitializedとprintされる
unowned参照とImplicitly Unwrapped Optionalプロパティ
Implicitly Unwrapped Optionalは、アクセスするときにnilだったらランタイムエラーでアプリがクラッシュするプロパティ、変数のことです。
Optional型ではないため、Unwrapせずに値を使用することができます。
詳しくは、以下を参照してください。
The Swift Programming Language (Swift 2.1): The Basics
使用するパターン
以下の様なパターンが発生することがあります。ほとんど無いですが。
- AはBの参照が必要
- BはAの参照が必要
unowned参照とImplicitly Unwrapped Optionalプロパティサンプルコード
The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。
class Country {
let name: String
var capitalCity: City! // Implicitly Unwrapped Optionalなので、解放済みのときにアクセスするとランタイムエラーでアプリがクラッシュ
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country // unownedなので、解放済みのときにアクセスするとランタイムエラーでアプリがクラッシュ
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
Closureと循環参照
Closureも参照型なので、循環参照が発生することがあります。
Closureで循環参照が発生するのは以下のパターンです。
- HogeクラスのインスタンスにClosureへのstrong参照のプロパティがある
- 1.のClosure内で、Hogeクラスのプロパティ、メソッドにアクセスしている
Closureで循環参照サンプルコード
The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。
class HTMLElement {
let name: String
let text: String?
// プロパティでasHTMLへのstrong参照を持っている
lazy var asHTML: Void -> String = { // nameとtextを使いたいからlazy
// Closure内でselfのプロパティをキャプチャしている
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
筋肉クイズ1
突然ですが、筋肉クイズの時間です。
closure_sample.swiftのクラス定義がある時...
var heading:HTMLElement? = HTMLElement(name: "h1")
let defaultText = "some default text"
heading!.asHTML = {
return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())
// <h1>some default text</h1>とprintされる
heading = nil
// ★問題★ ここではどうなるでしょう?
// (A) h1 is being deinitializedとprintされる
// (B) なにもprintされない
筋肉クイズ2
closure_sample.swiftのクラス定義がある時...
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// <p>hello, world</p>とprintされる
paragraph = nil
// ★問題★ ここではどうなるでしょう?
// (A) p is being deinitializedとprintされる
// (B) なにもprintされない
解答はキャプチャリストの後で
キャプチャリスト
Swiftではキャプチャリストという文法で、Closure内で使用するプロパティ、定数、変数のキャプチャの仕方(weak, unowned)を指定することができます。
詳しくは、以下を参照してください。
The Swift Programming Language (Swift 2.1): Expressions
引数、戻り値がある場合
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!, userData] (index: Int, stringToProcess: String) -> String in
// Closure内の処理
}
引数、戻り値がない場合
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!, userData] in
// Closure内の処理
}
筋肉クイズ1の解答
答えは...(A) p is being deinitializedとprintされる。
原因はよくわかってないです...(だれか教えて下さい)
キャプチャリストを使って以下のように書き換えると、何もprintされなくなります。
heading!.asHTML = {
[heading] in
return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
@_tid_ さんがBox化されているという解釈で説明してくれました!
Swift - クロージャのメモリ管理について - Qiita
筋肉クイズ2の解答
答えは...(B) なにもprintされない
asHTMLのイニシャライザでセットされているクロージャ内でselfがキャプチャされるためです。
HTMLElementのプロパティasHTMLを以下のように書き換えると、p is being deinitializedとprintされるようになります。
lazy var asHTML: Void -> String = {
[weak self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
言いたかったこと
(2015/12/18 追記)
- 以下の2つの条件を満たすと循環参照する可能性がある
- プロパティにクロージャを持つ
- クロージャ内でプロパティの持ち主を参照する
- コーディング(コードレビュー)している時にどのパターンで循環参照するか考えるのは大変すぎる
- プロパティのクロージャからプロパティの持ち主を参照するときは必ず、キャプチャリストでweakかunownedをつけていると安心できる
- コーディング規約で縛ろう💪
筋肉コーディング規約
あなたの筋肉をつかって、チームのコーディング規約に以下をコピペしましょう
('を`に置換してください)
## キャプチャリスト
循環参照の発生を防ぐために、プロパティにClosureを宣言するときは、
キャプチャリストで'''weak'''または'''unowned'''を指定すること💪💪💪
### Closure内でselfを参照する場合
'''
class HTMLElement {
// (中略)
lazy var asHTML: Void -> String = {
[weak self] in // ←💪
if let text = self!.text {
return "<\(self!.name)>\(text)</\(self!.name)>"
} else {
return "<\(self!.name) />"
}
}
}
'''
プロパティのClosureのasHTMLがselfを参照するので'''[weak self]'''
### Closureの所有者とClosureの参照先が同じとき
'''
var paragraph: HTMLElement = HTMLElement()
paragraph.asHTML = { [weak paragraph] in // ←💪
guard let paragraph = paragraph else {
return ""
}
return "<\(paragraph.name)>\(paragraph.text)</\(paragraph.name)>"
}
'''
paragraphがasHTMLを参照し、asHTMLがparagraphを参照するので'''[weak paragraph]'''
参照カウントについてもっと詳しく知りたい人へ
参照カウントの実装は、このあたりにありそうです。
swift/HeapObject.cpp at master · apple/swift · GitHub
おわりに
これであなたも立派な筋肉Swiftプログラマーです。
明日から、ダンベル片手に出社しましょう。