1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

循環参照の理解から対策まで

Last updated at Posted at 2024-01-28

循環参照の理解から対策までを記述しました。

ステップ1:メモリの仕組みを理解する

SwiftのメモリはARC(Automatic Reference Counting)と呼ばれる仕組みで管理されています。
ARCはオブジェクトを参照する数を数え、その数がゼロになると、そのオブジェクトは不要と見なされ、メモリから解放される。
これによって、プログラムが無駄なメモリを使わずに効率的に動作できます。

メモリの確保

ARCがメモリを確保するのはクラスのインスタンスを生成したタイミングである。
そこから変数に格納されるとそのメモリ(インスタンス)の参照という形で参照数がカウントされる

以下、ゲームに例えてコードで実装してみる。

// キャラクタークラス
class Character {
    var name: String
    init(name: String) {
        self.name = name
    }
}
// メモリ上に勇者(オブジェクト)を生成。変数に格納されため、キャラクタークラスの参照数は1
var character1:Character? = Character(name: "hero")
// 参照数は1

再度、キャラクタークラスをインスタンス化して別の変数に格納する。参照カウントは2となります。

// メモリ上にスライム(オブジェクト)を生成。変数に格納されため、キャラクタークラスの参照数は2
var character2:Character? = Character(name: "slime")
// 参照数は2

Characterクラスは2つの変数から参照されている。
Swiftのクラスは参照型でデータを保持しているのでこの参照カウントはクラスに対してのみ行われます。

メモリの解放

インスタンスに対して1つでも参照しているものが存在する限りは、インスタンスを解放しない。インスタンスへお参照がゼロになったときに初めてメモリを解放します。

// character1にnilを代入したが、これではSwiftはメモリを開放しない
var character1:Character? = nil

// character2にnilを代入したことにより、参照がゼロになり初めてメモリを解放する
var character2:Character? = nil

上記の説明でswiftのメモリの仕組みが理解できたので、次は本題の「循環参照」について考えます。

ステップ2:循環参照とは

2つのインスタンス同士がお互いを参照しあっている関係を循環参照と呼ぶ。互いを参照しあっているので参照カウンタがゼロにならず、メモリを解放することができない。

本来もう使用することはないデータがメモリ内に永続的に残っていてしまい、不要なメモリが解放されない状態が続いてしまっている。

以下のコードで説明します。

PersonクラスとPetクラスがあります。
Personはペットを所有する(petプロパティ)、また、Petには飼い主がいる(ownerプロパティ)。

// 人間
class Person {
    var name: String
    var pet: Pet?

    init(name: String) {
        self.name = name
    }
}

// ペット
class Pet {
    var species: String
    var owner: Person?

    init(species: String) {
        self.species = species
    }
}

// メモリ上にjohn(オブジェクト)を生成。
var john: Person? = Person(name: "john")
// Personインスタンスの参照数:1

// メモリ上にdog(オブジェクト)を生成。
var dog: Pet? = Pet(species: "Dog")
// Petインスタンスの参照数:1

// johnのpetプロパティにdogを代入。
john?.pet = dog
// Personインスタンスの参照数:2

// dogのownerプロパティにjohnを代入。
dog?.owner = john
// Petインスタンスの参照数:2


上記により以下の関係性が設定されます。
プロパティにインスタンスを代入したことによって、下記の図のようにお互いを参照し合う関係になった。

変数johnにnilを代入して、Swiftのメモリを開放してみるが・・・

// nilを格納しても参照は0にならない
john = nil

この場合変数johnにnilを格納(Personインスタンスの参照を解放)しても、PersonインスタンスはPetインスタンスのプロパティに紐づいているため参照は0にならない。
Personインスタンスは、dog?.owner = johnにより、犬の飼い主として設定されている状態。(参照されている)

さらにこのまま変数dogにもnilを格納する。

それぞれのインスタンスを指す変数はもうnilが代入されているので、これらのインスタンスにアクセスする手段は無く宙ぶらりんの状態で不要にメモリを占有しメモリリークしています。(循環参照)

// nilを格納しても参照は0にならない
dog = nil

上記により以下の関係性が設定されます。
インスタンスの循環参照が残り続けてしまいます。

ステップ3:循環参照の対策

循環参照の対策について説明します。
キーワードになるのが弱参照、強参照になります。

強参照:storong

ARCではインスタンスとデータの参照は基本的に「強参照」と呼ばれる強い参照で紐付けられ参照カウントが増やされます。そしてそのデータに対して強参照が残っている(カウントが0でない)限りメモリが解放されることはなく、また別インスタンスからの強参照により循環参照などが発生する原因になります。

弱参照:weak

強参照とは違い、弱参照は参照カウントを増やしません
参照しているオブジェクトが解放されると、弱参照は自動的にnilになります。

このweakキーワードをプロパティ宣言の前に付与することで参照の強さを変更することができます。

以下、コードで検証してみましょう。

Petクラスのプロパティにweakキーワードを付与します。
また、値としてnilを許容する必要があるので型はオプショナル型である必要があります。

ついでにデイニシャライザが起動していることを確認するため、print文も追加しておきます。
※デイニシャライザは、インスタンスが解放される直前に呼び出されます。

// 人間
class Person {
    var name: String
    var pet: Pet?

    init(name: String) {
        self.name = name
    }

    // デイニシャライザを定義する
    deinit {
        print("Personクラスのインスタンスが解放されました")
    }
}

// ペット
class Pet {
    var species: String
    /// 弱参照させる ///
    weak var owner: Person?

    init(species: String) {
        self.species = species
    }
    // デイニシャライザを定義する
    deinit {
        print("Petクラスのインスタンスが解放されました")
    }
}

weakを使うと、ARCの参照カウンタにカウントされずに、参照をすることができます。

var john: Person? = Person(name: "john")
// Personインスタンスの参照数:1
var dog: Pet? = Pet(species: "Dog")
// Petインスタンスの参照数:1
john?.pet = dog
// Personインスタンスの参照数:1のまま
dog?.owner = john
// Petインスタンスの参照数:2

上記により以下の関係性が設定されます。
weak使用前と比較すると、Personインスタンスの参照カウントが1減って1になりました。

ここで、変数johnにnilを代入すると、Personクラスのインスタンスが解放されます。

print("------------------------------")
john = nil
print("------------------------------")

// ------------------------------
// Personクラスのインスタンスが解放されました
// ------------------------------

上記により以下の関係性が設定されます。
Personクラスのインスタンスは最初の状態で参照カウントが1だったため、変数johnにnilを代入した結果、Personクラスのインスタンスの参照カウントが0になり、Personクラスのインスタンスは破棄されます。

そして、Petクラスのインスタンスへの参照カウントが1減って、1になります。また、Petクラスのインスタンスが参照するPersonクラスのインスタンスへのプロパティは弱参照となっているため、Personクラスのインスタンスの破棄に伴いnilが設定されます

この後は、変数dogにnilを代入することで、Petクラスのインスタンスの参照カウントが0になり破棄されます。これでメモリリークすることはなくなりました。

print("------------------------------")
dog = nil
print("------------------------------")

// ------------------------------
// Petクラスのインスタンスが解放されました
// ------------------------------

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?