楽水

人々の創造が自由に表現できる舞台づくり

Swift

Swiftのメモリ管理【weakやunownedをわかりやすく解説】

投稿日:


Swiftのプログラムでweakやonwnedというキーワードをみたことはありませんか?
これはSwiftが採用しているARCというメモリ管理の仕組みに関係しています。
今回は以下の観点でSwiftのメモリ管理について丁寧に解説します。

  • ARCとは何か
  • ARCのメリットとデメリット
  • なぜweakやunownedを使う必要があるのか
  • weakやunownedをどのように使うのか

参考本
[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

ARCとは何か

ARCとはAutomatic Reference Counting(自動参照カウント)の略で、Swiftが採用しているメモリ管理の仕組みのことです。
これは、参照型のインスタンス(オブジェクト)が各々どれだけ他から参照されているかを常にカウントしておき、どこからも参照されなくなったら(カウントが0になったら)、そのインスタンスをメモリ上から解放するというガベージコレクションの一方法です。
なお、構造体や列挙型など値型が渡されるときは、常にコピーされるため、自動参照カウントされません。
※ガベージコレクション
ガベージコレクションを簡単に説明すると、メモリ上にあるインスタンス全部の中から実行中のプログラムから参照されていない不要なインスタンスを探し出しそれを解放するという仕組みのことです。

ARCのメリットとデメリット

ガベージコレクションというとJavaやJavaScriptなど他の言語でも採用されていますが、ARCは通常の方法に比べ以下のメリットとデメリットがあります。

メリット

通常のガベージコレクションの場合、解放タイミングが自動で判断されるので、インスタンスが不要になってもすぐに解放されるとは限らず、実際にメモリから解放されるタイミングは不明です。
なので実際の解放処理が行われるまでの間、不要になったインスタンスはメモリに残ったままになり、その間、メモリが解放されず資源効率が悪くなります。
しかし、ARCの場合、インスタンスがどこからも参照されなくなったら、その直後にメモリからインスタンスが解放されるので比較的資源効率が良くなります。
特にモバイルアプリの場合、利用できるメモリ容量が限られているので、メモリが枯渇すると、動作が遅くなったりアプリが強制終了してしまうといったことが起こります。
そこで比較的資源効率が良いARCを採用することで限られたメモリ容量によるリスクを軽減しているのです。

デメリット

一方、ARCの場合、変数にインスタンスを代入したり、メソッドの引数として渡したり、その変数がスコープを抜けたりする度に参照カウンタのチェック処理が入るので、その分、オーバーヘッドがかかるというデメリットがあります。
また、インスタンス同士がお互いの参照を保持し合っていると、循環参照という状況に陥り、いつまで経っても参照カウントが0にならずメモリが解放されないのでメモリリークを起こしてしまう可能性が高くなります
したがって、SwiftなどARCを採用しているプログラム言語では循環参照を回避する何らかの仕組みを用意する必要があります
なお、通常のガベージコレクションの場合、循環参照になっていても丸ごと解放されるのでメモリリークは発生しません。

なぜweakやunownedを使う必要があるのか

先ほど説明したようにARCの大きなデメリットとして循環参照という問題があります。
これを回避するための解決方法としてSwiftでは参照の度合をコントロールするという方法を採用しています。
Swiftでは参照の度合を

  • 強い参照
  • 弱い参照(weak)
  • 非所有参照(unowned)

の3種類で区別しています。
このうち、強い参照は通常の参照のことで、循環参照を防ぐために使うのが弱い参照と非所有参照です。
プログラマーは循環参照の状況を考えた上で、弱い参照と非所有参照を使い参照をコントロールする必要があります。

weakやonownedをどのように使うのか

それでは、具体的にweakとunownedの使い方について見ていきましょう。

まず、循環参照がどのように起こるのか説明します。
このようなモデルを考えてみましょう。


※モデルはUMLのクラス図で作成しています。
このモデルを見ると、アパートに常にテナントが入っているわけではなく、人が常にアパートを利用しているわけではないので、それぞれれの多重度がオプショナル(任意)になっています。
これをSwiftで実装すると以下のようになります。

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 プロパティと、初めは nil のオプショナル なapartment プロパティを持ちます。
常にアパートがあるわけではないため、apartment プロパティをオプショナルにしています。

※Swiftのオプショナルについては、Swiftのオプショナルという記事を参照してください。

同じように、Apartment インスタンスは String 型の unit プロパティと、初めは nilのオプショナル なtenant プロパティを持ちます。
常にテナントがいるわけではないため、tenant プロパティをオプショナルにしています。
また、両クラスともにインスタンスが、デイニシャライズされたことを出力するデイニシャライザを定義しています。
これによって、Person と Apartment のインスタンスが期待通りに割り当て解除されたかどうかを確認することができます。
そこで、まず、以下のように各インスタンスを生成します。

var john: Person?
var unit4A: Apartment?
john = Person(name: “John Appleseed”)
unit4A = Apartment(unit: “4A”)

次にApartment インスタンスにPerson インスタンス、Person インスタンスをApartment インスタンスに割り当てます。

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

オプショナル値であるjohnのApartmentインスタンスを強制アンラップしてアクセスするためにエクスクラメーションマーク (!) を使っています。
さて、このように互いに参照を持たせることで以下のように、Person インスタンスと Apartment インスタンスが互いに「強い参照」を持つ循環参照が発生します。


ここで、以下のように変数とインスタンスの間にある「強い参照」を解除します。

john = nil
unit4A = nil

すると以下のように、循環参照のためPerson インスタンスと Apartment インスタンス間に強い参照が残ったまま切れなくなっている状態になります。


このように強い参照の循環参照に陥るといつまで経っても参照カウントが0にならずメモリが解放されないのでメモリリークを起こしてしまう可能性が高くなります。

それでは、この強い参照の循環参照をどのように解決するか見ていきましょう。
Swift には、クラスのプロパティを扱うときに、強い参照の循環を回避する方法が 2 つあります。
それは、弱い参照と非所有参照です。
弱い参照と非所有参照は、あるインスタンスが、別のインスタンスの参照を強く保持することなく、そのインスタンスを参照できるようにする循環参照です。
従って、強い参照の循環を生成することなく、インスタンスが互いに参照することができます。
この2つを選択するときの判断記述は以下です。

  • 生存期間のどこかで参照が nil になることが妥当な場合に弱い参照を利用する
  • 初期化時に設定されると、その後、相手に対する参照が nil になることがないとわかっている場合には、非所有参照を利用する

それぞれ、具体的に見ていきましょう。

弱い参照の使い方

弱い参照は、参照するインスタンスを強く保持しない参照のことです。
なので、弱い参照がまだ参照している間に、そのインスタンスが割り当て解除されることがあります。
参照するインスタンスが割り当て解除されたとき、ARC は自動的に弱い参照に nil を設定します。
弱い参照の値を nil にできるようにするため、弱い参照は常にオプショナルな型とします。
先ほどの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 }
weak var tenant: Person?
deinit { print(“Apartment \(unit) is being deinitialized”) }
}

弱い参照とするには、プロパティや変数の宣言の前に weak キーワードを置きます。
この場合、Apartment 型の tenant プロパティが弱い参照として宣言されています。
また、弱い参照の値を nil にできるようにするため、tenant プロパティはオプショナルな型としています。
前回と同様、Person インスタンスと Apartment インスタンスが循環参照するようにしてみましょう。

var john: Person?
var unit4A: Apartment?john = Person(name: “John Appleseed”)
unit4A = Apartment(unit: “4A”)john!.apartment = unit4A
unit4A!.tenant = john
//オプショナル値であるjohnのApartmentインスタンスを強制アンラップしてアクセスするためにエクスクラメーションマーク (!) を使っています。

そうすると下図のような状態になります。

前回どおり、2 つの変数(john と unit4A)と 2 つのインスタンス間のつながりから強い参照が生成されます。
また、Person インスタンスは依然として Apartment インスタンスに対する強い参照を持ちますが、Apartment インスタンスは Person インスタンスに対して弱い参照を持ちます。
ここでjohn 変数に nil を設定して強い参照を切ってみましょう。

john = nil
// “John Appleseed is being deinitialized” と出力

そうすると下図のような状態になります。

まずjohn 変数に nil を設定して強い参照を切ると、Person インスタンスに対する強い参照は無くなります。
そうするとPerson インスタンスが保持していたApartment インスタンス対する強い参照も無くなります。
その結果、ARCはApartment インスタンスの tenant プロパティに nil を設定して弱い参照を解除します。
今度は、変数 unit4A からApartment インスタンスに対する強い参照を切ってみますよう。

unit4A = nil
// “Apartment 4A is being deinitialized” と出力

その結果、最終的に下図のような状態になります。

このように生存期間のどこかで参照が nil になることが妥当な場合に弱い参照を利用します。
例えば、上のクラス図のように多重度がオプショナル(任意)の関係になっている場合、弱い参照を適用することを考えます

非所有参照の使い方①

弱い参照と同様、非所有参照は、参照するインスタンスを強く保持することはありません。
しかし、弱い参照と異なり、非所有参照には常に値があり常に参照されるものと想定されます
なので、非所有参照は常にオプショナルでない型で定義されます
しかし、オプショナルでない型の変数に nil を設定することはできないため、参照するインスタンスが割り当て解除されたときに、ARC は参照に nil を設定することができません。
このような場合、強い参照による循環参照を防ぐために非所有参照を使います。

今度は次のようなモデルを考えます。

このモデルを見ると、常に人がクレジットカードを持っているわけではないので顧客に対するクレジットカードの多重度はオプショナル(任意)になっていますが、クレジットカードには必ず、それを所有する顧客がいるのでクレジットカードに対する顧客の多重度は1(必須)になっています。
今回はクレジットカードに対する顧客のように、初期化時に設定されると、その後、相手に対する参照が nil になることがないとわかっている場合、どのように非所有参照を使うのか見ていきましょう。
先ほどのクラス図をSwiftで実装すると以下のようになります。
非所有の参照とするには、プロパティや変数の宣言の前に unowned キーワードを置きます。
以下の場合、CreditCardクラスのcustomerがunowned になります。

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 クラスは card プロパティをオプショナルとしていますが、クレジットカードには常にそれを保持する顧客がいるので、CreditCard クラスはオプショナルでない customer プロパティとしています。
さらに、新しい CreditCard インスタンスは、CreditCard イニシャライザに number 値と customer インスタンスを渡すことでしか生成できません。
これにより、CreditCard インスタンスが生成されるときに、CreditCard インスタンスに結びつけられる Customer インスタンスが常にあるようになります。
つまり、CreditCard インスタンス初期化時に設定されると、その後 Customer インスタンスに対する参照が nil になることがないということです。
このように、クレジットカードには常に顧客があるため、customer プロパティを強い参照の循環を避ける非所有参照として定義します。
次に実際にインスタンスを生成します。

var john: Customer?
john = Customer(name: “John Appleseed”)
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
//オプショナル値である johnのCreditCard インスタンスを強制アンラップしてアクセスするためにエクスクラメーションマーク (!) を使っています。

その結果、以下のような状態になります。

Customer インスタンスは CreditCard インスタンスに対する強い参照を持ち、CreditCard インスタンスは Customer インスタンスに対する非所有参照を持っています。
そこで、変数 john による強い参照を切ると、Customer インスタンスに対する強い参照は無くなり、ARCによりCustomer インスタンスが無くなります。
この結果、CreditCard インスタンスに対する強い参照も無くなるため、CreditCard インスタンスが非所有で参照していいるCustomer インスタンスの割り当ても解除されます。

john = nil
// “John Appleseed is being deinitialized” と出力
// “Card #1234567890123456 is being deinitialized” と出力

このように初期化時に設定されると、その後、相手に対する参照が nil になることがないとわかっている場合には、非所有参照を利用します。
例えば、上のクラス図のように、どちらかの多重度が必須の関係になっている場合、非所有参照を適用することを考えます

非所有参照の使い方②

これまで

  • 生存期間のどこかで参照が nil になることが妥当な場合に弱い参照を利用する
  • 初期化時に設定されると、その後、相手に対する参照が nil になることがないとわかっている場合には、非所有の参照を利用する

について見てきました。
Person と Apartment の例は、2 つのプロパティが共に nil になることがあり、強い参照の循環になる可能性がある状況を示していました。
このシナリオの場合には、弱い参照で解決することがベストです。
Customer と CreditCard の例は、一方のプロパティは nil になることがあり、他方は nil になることがない、強い参照の循環になる可能性がある状況を示していました。
このシナリオの場合には、非所有参照で解決することがベストです。
しかしながら、両方のプロパティに常に値があり、初期化完了後にはプロパティが nil になることが無いという 3 つ目のシナリオがあります。
このシナリオの場合には、一方のクラスを非所有参照とし、他方のクラスを無条件にアンラップされるオプショナルプロパティとして組み合わせることが効果的です。
最後に、このシナリオについて見ていきましょう。
まず、下のクラス図のモデルを見てください。

このモデルでは国には必ず首都があり、首都には必ず国があるという関係になっています。
これをSwiftで実装すると以下のようになります。

class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}

class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}

Country クラスには capitalCity プロパティがあり、City クラスには country プロパティがあります。
Cityインスタンスには常にCountryインスタンスがあるため、country プロパティを強い参照の循環を避ける非所有参照として定義します。
2 つのクラス間の相互依存をセットアップするために、City のイニシャライザは Country インスタンスを取り、country プロパティでこのインスタンスを保持します。
City のイニシャライザは Country のイニシャライザ内から呼び出されます。
しかし、新しい Country インスタンスが完全に初期化されるまで、Country のイニシャライザは City のイニシャライザに self を渡すことはできません。
この問題に対処するために、Country クラスの capitalCity プロパティを、タイプアノテーションの最後にエクスクラメーションマークを付ける (City!) ことで、無条件にアンラップされるオプショナルとして宣言します。
capitalCity プロパティは、他のオプショナルと同じように nil がデフォルト値ですが、値をアンラップする必要なくアクセスすることができます。
capitalCity のデフォルト値が nil であるため、Country インスタンスがイニシャライザ内で name プロパティを設定するとすぐに、新しい Country インスタンスは完全に初期化されているとみなされます。
つまり、name プロパティが設定されるとすぐに、self プロパティを参照し、渡すことができるようになります。
従って、Country イニシャライザが capitalCity プロパティを設定するとき、Country イニシャライザは City イニシャライザのパラメータの 1 つとして self を渡すことができます。
このとき、先にも述べたように、capitalCity プロパティは、無条件にアンラップされるオプショナルなので値をアンラップする必要なくアクセスされています。
以上より、CountryクラスとCityクラスは初期化時に互いのインスタンスを保持することになります。
つまり、両方のプロパティに常に値があり、初期化完了後にはプロパティが nil になることが無いということです。
それでは、Countryインスタンスを生成してみましょう。

var country = Country(name: “Canada”, capitalName: “Ottawa”)
print(“\(country.name)’s capital city is called \(country.capitalCity.name)”)
// “Canada’s capital city is called Ottawa” と出力

ここでCountryインスタンスを削除すると

country = nil

Countryインスタンスが保持しているCityインスタンスに対する強い参照が消え、Cityインスタンスが非所有で参照していいるCountryインスタンスの割り当ても解除されます。

以上、今回はSwiftのメモリ管理であるARCの仕組みと、そのデメリットである強い参照の循環参照をweakやunownedを使って回避する方法について解説しました。

-Swift
-, , ,

執筆者: