28
30

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 3 years have passed since last update.

Swiftのメモリリークについてまとめてみた

Last updated at Posted at 2020-03-02

普段delegateを宣言するときにweakをつけていたり、Closureを宣言するときに[weak self]を付けたりと、本来の目的であるメモリリークのことはあまり深く意味は考えずに使ってしまっていたので、本記事でまとめて意識的に使えるように自分の中で整理することにしました。

メモリリークとは

Appleの公式ドキュメントによると、

Memory leaks are blocks of allocated memory that the program no longer references.

と記載されており、プログラムが二度と参照しないが、割り当てられたメモリのブロックのことです。

もう少し分かりやすく

あるオブジェクトをメモリに確保したあと、そのオブジェクトが解放されずにメモリ内に残ってしまう状態のことです。
仮に解放されなかったオブジェクトが1つであったとしても、そのオブジェクトが他のオブジェクトを参照していれば、多くのリークが溜まってしまい、これがメモリリークの怖いところです。

メモリリークが引き起こす問題

上記の文章の続きに、

Leaks waste space by filling up pages of memory with inaccessible data and waste time due to extra paging activity.

と記載されており、メモリリークの説明と少し被りますが、アクセス不能なデータでメモリを埋め尽くし、余計なページングアクティビティによって時間を無駄にしてしまうことが問題である、ということです。

もう少し詳しく

ここに記載されている

It will never die and it will never stop listening to the notification. Each time the notification is posted, the object will react to it. If the user repeats an action that creates the object in question, there will be multiple instances alive. All those instances responding to the notification and stepping into each other.

例のように、通知を受け取るオブジェクトがメモリリークとしての生成され続けてしまった場合、全てのオブジェクトが通知を受け取ることになってしまいます。
そしてそれはアプリがクラッシュに繋がるのです。

メモリリークの原因

原因は、循環参照と呼ばれるものです。

循環参照とは

循環参照とは、関連のあるクラスのインスタンス同士を互いに強参照した場合に起こる。
循環参照は、以下のようなシチュエーションで生じます。(参照カウントに関しては後述します)

public class Wizard {
    public var wand: Wand?
    public let name: String

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

public class Wand {
    public var wizard: Wizard?
    public let name: String
    
    init(name: String) {
        self.name = name
    }
}

var harry: Wizard? = Wizard(name: "harry") // harryの参照カウントが+1
var elder: Wand? = Wand(name: "Elder") // elderの参照カウントが+1
harry?.wand = elder // elderの参照カウントが+1
elder?.wizard = harry // harryの参照カウントが+1
harry = nil // harryの参照カウントが-1(最終的な参照カウントは1)
elder = nil // elderの参照カウントが-1(最終的な参照カウントは1)
// 循環参照されており、参照カウントがどちらも0にならないため、解放されない。

ここに関しては既に沢山の記事で書かれているので、割愛します。
オススメ

Swiftにおけるメモリ管理

Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

上にも書かれている通り、Swiftでは、Automatic Reference Counting (ARC)という仕組みによって、我々がメモリ管理について深く考える必要がなく、内部的に勝手にメモリ管理を行ってくれます。

もう少し詳しく

Every time you create a new instance of a class, ARC allocates a chunk of memory to store information about that instance. This memory holds information about the type of the instance, together with the values of any stored properties associated with that instance.
Additionally, when an instance is no longer needed, ARC frees up the memory used by that instance so that the memory can be used for other purposes instead.

上にも書かれている通り、新たにクラスのインスタンスを作成した場合、ARCがそのインスタンスに関わる情報(プロパティなど)をまとめたものを、メモリに割り当ててくれます。そして、それらが必要がなくなった場合は、ARCがそのメモリを他の目的で使えるように、開放してくれます。

実際にどういう仕組みで、効率よくメモリの割り当て、解放をしているのか

強参照

本当はまだインスタンスのプロパティや関数にアクセスしたいのに、ARCが既にそのインスタンスへの割り当てを割り当てを取り消してしてまっていては、nilにアクセスすることになり、ランタイムクラッシュしてしまいます。
こういった状況を防ぐために、強参照という参照の仕方があります。また、Swiftではこの強参照がデフォルトになっているので、特に指定しなかった場合、強参照になります。

whenever you assign a class instance to a property, constant, or variable, that property, constant, or variable makes a strong reference to the instance. The reference is called a “strong” reference because it keeps a firm hold on that instance, and does not allow it to be deallocated for as long as that strong reference remains.

上にも書かれている通り、クラスのインスタンスを割り当てる度に、そのインスタンスへ強参照が作られる。
ex.

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

Personクラスに対して、

var reference1: Person? // まだインスタンス化していないので参照されない
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed") // インスタンス化したので、reference1がPersonインスタンスに強参照している。
// Prints "John Appleseed is being initialized"

// 新たに二つ強参照が確立される。
reference2 = reference1
reference3 = reference1

// nilを代入することによって、強参照を二つ破棄するが、まだ強参照が1つ残っているため、Personインスタンスの割り当ては取り消されない。
reference1 = nil
reference2 = nil

// 最後の強参照が破棄されたため、Personインスタンスのメモリ割り当てが取り消される。
reference3 = nil
// Prints "John Appleseed is being deinitialized"

(上のコードで出ていくる、強参照の数というのが、参照カウントである。)

上記のコードの例のように、インスタンスへの強参照の数が0になればメモリへの割り当てが取り消され、メモリリークが起こることはありません。しかし、この強参照の数が0にすることが出来ないという場合もあり、その例がここで紹介した循環参照です。そして、その循環参照を回避する方法が、以下にも書いてある通り、強参照を使うのではなく、クラス通しの関係を、weak(弱参照)またはunowned(非所有参照)で定義するというものです。

You resolve strong reference cycles by defining some of the relationships between classes as weak or unowned references instead of as strong references.

何故、参照カウントが0にならないのか、というところの説明で、ここの図がとても分かりやすかったです。

弱参照

これは、強参照せずに、つまり、参照カウントを増やすことなくインスタンスを参照できる仕組みです。
つまり、参照しているインスタンスの割り当てが破棄されたと同時に、ARCが自動的に弱参照のものにはnilを代入してくれます。
また、

because weak references need to allow their value to be changed to nil at runtime, they are always declared as variables, rather than constants, of an optional type.

上の通り、弱参照を用いて定義された変数は、ランタイムにおいてnilになる可能性が十分にあるため、オプショナル型である必要があります。

ここで、もう一度先ほどのコードで循環参照されないように書いてみようと思います。

public class Wizard {
    public var wand: Wand?
    public let name: String

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

public class Wand {
    public weak var wizard: Wizard?
    public let name: String
    
    init(name: String) {
        self.name = name
    }
}

var harry: Wizard? = Wizard(name: "harry") // harryの参照カウントが+1
var elder: Wand? = Wand(name: "Elder") // elderの参照カウントが+1
harry?.wand = elder // elderの参照カウントが+1
elder?.wizard = harry // harryの参照カウントは変化しない
harry = nil // harryの参照カウントが-1(最終的な参照カウントは0)

上記においては、public weak var wizard: Wizard?と宣言し、WizardインスタンスはWandインスタンスを強参照しているが、WandインスタンスはWizardインスタンスを弱参照しています。よって、herry = nilによって、Wizard, Wand間の強参照が全て破棄され、唯一のWizardインスタンスであるherryにもnilが入ったので、残る強参照は、elderからのWandインスタンスへの強参照のみとなり、無事に循環参照を回避することが出来ました。
よって、最後に

elder = nil

してあげると、全ての強参照を破棄することが出来ます。

非所有参照

An unowned reference is expected to always have a value. As a result, ARC never sets an unowned reference’s value to nil, which means that unowned references are defined using non-optional types.

と記載されているように、unownedで定義されるものはnilでないことが保証されたものでなければなりません。よって、強参照を避ける場合は、unownedではなくweakを使う方が安全です。
では、どういった場合にunownedを使うのか。それは次のように、片方のClassのインスタンスを代入するためのPropertyはOptionalにすべきだが、もう片方のClassのpropertyにはクラスのインスタンスが値としてはいる(nilでない)ことが保証されている場合です。

class Customer {
    let name: String
    var card: CreditCard? // Optional
    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
    }
    deinit { print("Card #\(number) is being deinitialized") }
}
2つのクラスのpropertyに確実に値が入ることが保証されている場合

このような場合は、unownedImplicitly Unwrapped Optionalsを組み合わせて、循環参照にはならずに、下で言うcapitalCityとcountryがnilでないことを保証しています。

class Country {
    let name: String
    var capitalCity: City! // Implicitly Unwrapped Optionals
    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
  • delegate

Closureにおける循環参照

Class内でClosureを定義した場合、ClassはClosureを参照しており、ClosureもClassを参照する。
よって、何も考えずにどちらも強参照した場合には、前述したような循環参照が発生してしまうのである。
また、何故PropertyやVariableではこういった循環参照が起きないのに対して、ClosureやClassでは循環参照(強参照)が起きるのかというと、使われなると自動的に解放される値型と、より汎用的に使用されるため、ARCによって参照を管理されている参照型という分類があるためである。一応分類だけは紹介するが、それぞれに対する深掘りは今回は割愛する。

参照型

  • Class
  • Closure

値型

  • Variable
  • Property

公式では、使っているClass名は本記事とは異なるものの、

The instance’s asHTML property holds a strong reference to its closure. However, because the closure refers to self within its body (as a way to reference self.name and self.text), the closure captures self, which means that it holds a strong reference back to the HTMLElement instance.

と書かれており、そのClosureをPropertyとして持つクラスがインスタンス化された場合、そのインスタンスがClosureに強参照しており、Closure内でselfにアクセスする以上は、インスタンスがClosureに強参照することになるため、循環参照が発生する。ただし、以下に書かれている通り、

Even though the closure refers to self multiple times, it only captures one strong reference to the HTMLElement instance.

Closure内で何回selfにアクセスしても、Closureがインスタンスを強参照する回数は変わらない。

Closureにおける循環参照の例

class MyClass {
    lazy var myClosure: () -> Void = { // 実行時にselfが暗黙的に強参照としてキャプチャされてしまう
        print(self.title)
    }
}

var myClass: MyClass? = MyClass() // 参照カウンタ+1 = 1
myClass?.myClosure() // ここでselfが強参照としてキャプチャされる、参照カウンタ+1 = 2
myClass = nil        // 参照カウンタ-1 = 1

Closureにおける循環参照の対処例

これに対してはClass同士の循環参照の時と同様に、weakunownedで対処してあげれば良い。

class MyClass {
    lazy var myClosure: () -> Void = { [weak self] in // selfを明示的にweakとしてキャプチャする
        print(self?.title)
    }
}

var myClass: MyClass? = MyClass() // 参照カウンタ+1 = 1
myClass?.myClosure() // ここでselfがweakとしてキャプチャされるので、参照カウンタ+0 = 1
myClass = nil        // 参照カウンタ-1 = 0となり、解放される。

ここで、weakを使うかunownedを使うかの疑問があると思うが、unownedを使った場合はnilでないことが保証されている場合のみなので、**guard let self = self else { return }**などとして、ランタイムクラッシュのリスクテイクをした方が安全である。

delegateにおける循環参照

以下、ここを参照します。

弱参照を使う

To prevent strong reference cycles, delegates are declared as weak references.

delegateをプロパティに定義する場合は、循環参照を避けるために、weakをつけて宣言しなければなりません。

weak var delegate: SomeProtocol?

ProtocolはAnyObjectを継承する

少し本題と外れますが、delegateを使うためのProtocolを宣言する際、ProtocolはClass-Only Protocolでなければなりません。詳しくはここら辺の記事に書かれていましたが、weakというのはそもそも値型には使えず、参照型にしか使えないという前提があり、参照型にしか使わないという制約をつけてあげなければなりません。そして、その制約をつけてあげるために、Class-Only Protocolである必要があり、

A class-only protocol is marked by its inheritance from AnyObject

にあるように、

protocol ProtocolDelegate: AnyObject {}

といったようにProtocolを宣言してあげれば良いです。

参考文献

28
30
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
28
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?