2019/12/17 Swift 自動参照カウント(Automatic Reference Counting)

Swiftはメモリ管理のために自動参照カウント(Automatic Reference Counting)を使用している。そのためほとんどのケースで、私たちが自分でメモリ管理について考える必要はない。クラスインスタンスがもう必要ない状況になるとARCがそのクラスインスタンスに使われているメモリを解放する。

 

しかしわずかなケースでARCは、メモリ管理のためにコードの中のパーツの関係性に関する情報を必要とする場合がある。この章ではそのようなシチュエーションについて触れ、全てのアプリのメモリ管理のやり方について説明する。

 

参照カウントはクラスインスタンスに関してのみ適用される。構造体や列挙型は値型であり参照型ではないので、参照によって代入や参照渡しが行われるわけではない。

ARCはどのように機能するのか

クラスインスタンスを生成した時は常に、インスタンスに関する情報を格納するためにARCがメモリのチャンクを割り当てます。このメモリは「インスタンスの型」の情報と、「そのインスタンスのストアドプロパティの値」に関する情報を保持する。

加えて、インスタンスが不要になった時、メモリを他の目的で使用できるように、それまでクラスインスタンスに使われていたメモリを解放する。これによりインスタンスが不要になった後もメモリが確保され続けるという状況が起こらないことが保証される。

不要になったインスタンスを割り当て解除(削除)するのは何の問題もないが、もしまだ使う可能性があるインスタンスをARCがメモリから削除してしまうと、そのインスタンスのプロパティやメソッドにアクセスすることができなくなってしまう。実際削除されたインスタンスにアクセスしようとするとアプリがクラッシュする可能性が高い。

インスタンスがまだ必要なのに削除される、ということがないように、ARCは、何個の「変数・定数・プロパティ」がそのインスタンスを参照しているかを数えている。少なくとも一つ以上有効な参照が存在するうちは、ARCがそのインスタンスを削除することはない。

これを可能にするために、クラスインスタンスを「プロパティ・定数・変数」に代入する時は、その「プロパティ・定数・変数」はクラスインスタンスに対する強い参照(強参照)を作成する。この参照はインスタンスに対して強固なホールド(保持)をキープするので「強参照」と呼ばれ、強参照があるうちはインスタンスが削除されることはない。

ARCの挙動

ここにARCの挙動のサンプルを示す。定数のストアドプロパティnameを持つシンプルなクラスPersonを定義する。

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

Personクラスはnameプロパティを初期化して、実行中であるメッセージを表示するイニシャライザを持つ。さらに、割り当て解除(削除)された時にメッセージを表示するデイニシャライザを持つ。

次のコードは、Person?(OptionalPerson型)の変数を三つ宣言している。これらの変数はさらに下のコードで、新しいPersonクラスのインスタンスへの複数の参照をセットされる。これらの変数はオプショナル型なので、自動的にnilがセットされる(初期化されていないので)。この段階ではPerson型のインスタンスを代入していないので参照はない。

var reference1: Person?
var reference2: Person?
var reference3: Person?

Personクラスのインスタンスを新たに生成し、変数reference1に代入する。

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

イニシャライザを実行すると“John Appleseed is being initialized”というメッセージが表示される。これによりイニシャライザが実行されたことを確認できる。

変数reference1に新しいPersonクラスのインスタンスが代入されたので、reference1からPersonクラスのインスタンスへの強参照が生まれた。少なくとも一つ以上の強参照が存在するうちは、ARCはこのPersonインスタンスをメモリに維持し削除しないことを保証する。

同じPersonインスタンスを他の二つの変数に代入すると、そのインスタンスへの強参照がさらに二つ生まれる。

reference2 = reference1
reference3 = reference1

今、一つのPersonインスタンスへの三つの強参照が存在する。

二つの変数(reference1,reference2)にnilを代入して、二つの強参照(オリジナルの強参照を含む)を壊すと、一つの強参照が残るので、Personインスタンスはまだ削除されない。

reference1 = nil
reference2 = nil

ARCは三つ目の(そして最後の)強参照が壊れない限りPersonインスタンスを削除しない。三つ目の(そして最後の)強参照が壊れた時にはPersonインスタンスを利用することはできなくなる。

reference3 = nil
// Prints "John Appleseed is being deinitialized"

クラスインスタンスの循環参照

上のサンプルで、ARCはあなたが生成した新しいPersonインスタンスへの強参照の数を追跡し、そのPersonインスタンスが必要なくなった時に削除した。

しかし、クラスのインスタンスへの強参照がゼロにならないようなコードを書くことができます。それは二つのクラスインスタンスがお互いに相手への強参照を持つような状況です。この状況は循環参照として知られます。

循環参照への対策として、クラスのインスタンス同士の関係として強参照の代わりにweak reference,あるいはunowned referenceを定義する方法があります。まず循環参照への対策の前に、どのように循環参照が発生するかを見てみましょう。

下のサンプルはアパートとその住人をモデルにしたPersonクラスとApartmentクラスを定義しています。循環参照のサンプルです。

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") }
}

PersonインスタンスはString型のnameプロパティと、Optional型で初期値がnilのapartmentプロパティを持ちます。人間は常にアパートを持っているわけではないのでapartmentプロパティはオプショナル型です。

全てのApartmentクラスのインスタンスはString型のunitプロパティを持ちます。それとオプショナル型で初期値nilのtenantプロパティを持ちます。tenantプロパティは常に住人がいるわけではないのでオプショナル型です。

両方のクラスにデイニシャライズされた事実を表示するデイニシャライザが定義されています。これによりPersonクラスインスタンスとApartmentクラスインスタンスが期待通りに割り当て解除(削除)されているかを見ることができます。

次のコードはPersonクラスインスタンスを格納するためのオプショナル型の変数johnと、Apartmentクラスインスタンスを格納するためのオプショナル型変数unit4Aを定義しています。どちらもオプショナル型で初期化していないので初期値はnilです。

var john: Person?
var unit4A: Apartment?

宣言した変数にPersonインスタンスとApartmentインスタンスを生成して代入します。

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

ここで、二つのインスタンスを生成し代入した時にどのように強参照ができるか見てみましょう。変数johnは新しいPersonインスタンスに対して強参照を持っています。変数unit4Aは新しいApartmentインスタンスに対して強参照を持っています。

../_images/referenceCycle01_2x.png

ここでPersonインスタンスのapartmentプロパティが値(Apartmentインスタンス)を持ち、Apartmentインスタンスのtenantプロパティが値(Personインスタンス)を持つようにして、二つのインスタンスをリンクさせることができます。

強制アンラップにより取り出したインスタンスのプロパティに相手インスタンスへの参照をセットします。

john!.apartment = unit4A
unit4A!.tenant = john

二つのインスタンスをリンクさせた時に強参照がどうなるかを以下に示します。

 

上記のように二つのインスタンスをリンクさせると循環参照が発生してしまいます。PersonインスタンスはApartmentインスタンスへの強参照を持ち、ApartmentインスタンスはPersonインスタンスへの強参照を持ちます。そのため、johnとunit4Aにnilを代入して、「変数johnからの強参照」と「変数unit4Aからの強参照」が無くなっても、参照カウントがゼロにならないので、二つのインスタンスはARCにより割り当て解除(削除)されることはありません。

john = nil
unit4A = nil

johnとunit4Aにnilを代入してもデイニシャライザが呼ばれないことに注目してください。循環参照により二つのインスタンスは永遠に削除されないので、メモリリークが発生してしまいます。

PersonインスタンスとApartmentインスタンスの間の強参照は残り、壊すことはできません。

クラスインスタンス間の循環参照の解決

Weak あるいは unowned referencesを使うと、上の例で循環参照になっていた一つのインスタンスが、強参照無しで別のインスタンスを参照することができます。強参照無しなので循環参照を回避しながらお互いを参照することができます。

weak referenceは、もう一つのインスタンスが自分よりもライフタイムが短い、つまり相手インスタンスの方が最初に割り当て解除される場合。上のApartmentの例では、アパートメントがライフタイムの中でテナントを持たないことが適切なので、このケースで循環参照を回避するにはweak referenceが適切な方法。それに対して、unowned referenceは相手インスタンスの方がライフタイムが長い場合に使います。

Weak References

weak referenceは強参照を持たない参照です。ですのでARCにより、参照しているインスタンスが削除されることがある、ということです。この振る舞いにより循環参照が回避できます。クラスのプロパティの定義の前にweakキーワードを記述することでweak referenceであることを示します。

参照しているインスタンスへの強参照を持たないのがweak refereneです。ですのでweak referenceが参照している最中にインスタンスが削除されることが可能になります。ですので参照しているインスタンスが削除された時、ARCはweak referenceにnilを自動的にセットします。weak referenceのプロパティの値が実行時にnilになることがあるので、weak referenceのプロパティは常にオプショナル型の変数として宣言します。

下のサンプルは一つの重要な点をのぞいて、上のPersonクラスとApartmentクラスのサンプルと同じものです。今回は、Apartment型のtenantプロパティがweak referenceとして宣言されています。

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?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

前回同様、二つの変数(johnとunit4A)からの強参照と、二つのインスタンスの間のリンクを作ります。

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

image: weakReference01_2x

PersonインスタンスはApartment インスタンスへの強参照を持っていますが、ApartmentインスタンスはPersonインスタンスへのweak referenceを持っています。変数johnにnilを代入して「johnからPersonインスタンスへの強参照」を壊せば、Personインスタンスへの強参照は無くなる、ということになります。

Personインスタンスへの強参照が無くなりますので、(ARCにより)Personインスタンスは削除され、(Apartmentインスタンスの)tenantプロパティにnilがセットされます。image: weakReference02_2x

唯一残っているApartmentインスタンスへの強参照は変数unit4Aからの強参照です。(変数unit4Aにnilを代入して)その強参照を壊すと、Apartmentインスタンスへの強参照が無くなります。

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

Apartmentインスタンスへの強参照も無くなりましたので、(ARCにより)Apartmentインスタンスも削除されます。image: weakReference03_2x

Unowned References

weak referenceと同様、unowned referenceも、強参照を持たずに相手インスタンスを参照します。しかしweak referenceと違う点は、相手インスタンスが自分と同じライフタイム、あるいは自分よりも長いライフタイムを持つ時にunowned referenceを使用するということです。unowned referenceを使用するときは、unownedキーワードをプロパティ・あるいは変数の前に記述して宣言します。

unowned referenceは常に値を持つことが期待されます。結果として、unowned referenceが指す値をARCがnilにすることはなく、よってunowned referenceは非オプショナル型として宣言します。

important

unowned referenceは、削除されないインスタンスを常に参照する場合に使用してください。unowned referenceが参照する値が削除された後にその値にアクセスしようとすると実行時エラーとなります。

下のサンプルはCustomerとCreditCardの二つのクラスを定義しています。銀行の顧客とクレジットカードをモデルとしています。両方のクラスがプロパティとしてもう片方のクラスのインスタンスを格納しています。この関係性らは循環参照が発生する可能性を持っています。

CustomerクラスとCreditCardクラスの関係性は、上のサンプルで出てきたPersonクラスとApartmentクラスの関係性と微妙に違います。このモデルでは顧客(Customer)はクレジットカードを持っている場合もあれば持っていない場合もありますが、クレジットカードは常に顧客と関連づけられます。(持ち主のないクレジットカードはない、ということ。)CreditCardインスタンスが参照しているCustomerインスタンスが消えているのに、CreditCardだけが存在し続ける、という状況はありません。この状況を表現するために、Customerクラスのcardプロパティはオプショナル型、CreditCardクラスのcustomerプロパティはunowned  referenceである非オプショナル型を持つようにします。

さらに、CreditCardインスタンスは、numberプロパティとcustomerプロパティに値とインスタンスをセットするカスタムイニシャライザによりインスタンスを生成するようにします。こうすることで、生成したCreditCardインスタンスは必ずcustomerインスタンスを持つことが保証されます。

クレジットカードは常に顧客を持ちますから、循環参照を回避するためにcustomerプロパティはunowned referenceとして定義します。

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
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

次のコードはオプショナルCustomer型の変数johnを宣言しています。特定の顧客(Customerインスタンス)への参照を格納するための変数です。この変数オプショナル型なのではデフォルト値はnilになります。(詳しくはSwift オプショナル型 パート1、sample2-3)

var john: Customer?

新しいCustomerインスタンスを生成し、そのCustomerインスタンスを用いて新しいCreditCardインスタンスを生成、そしてそのCreditCardインスタンスをjohnのcardプロパティに代入することができます。

二つのインスタンスをリンクさせたので、参照の関係性は以下のようになります。

image: unownedReference01_2x

CustomerインスタンスはCreditCardインスタンスに対して強参照を持ち、CreditCardインスタンスはCustomerインスタンスに対してunowned referenceを持ちます。

ここで変数johnにnilを代入すると、johnからCustomerインスタンスへの強参照が壊れます。CreditCardインスタンスのcustomerプロパティはunowned referenceですから、この時点でCustomerインスタンスへの強参照は無くなります。image: unownedReference02_2x

Customerインスタンスへの強参照が無くなりましたので、Customerインスタンスは削除をされます。そうするとCreditCardインスタンスへの強参照も無くなりますので、CreditCardインスタンスも削除されます。

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

上記のコードにより変数johnにnilが代入された後、二つのクラスのデイニシャライザが呼ばれたことが表示されます。

note

上のサンプルは安全なunowned referenceの使い方を示しています。それとは別に、例えばパフォーマンスの理由により、Swiftは実行時の安全性のチェックをできない安全でないunowned referenceも用意されています。

Unowned Referencesと暗黙的アンラップオプショナル型のプロパティ

上のweak referenceとunowned referenceのサンプルは、循環参照を回避する必要性を示す二つの一般的なシナリオをカバーしています。

Person and Apartmentのサンプルは、

 

 

参考

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

コメントを残す

メールアドレスが公開されることはありません。