普段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に確実に値が入ることが保証されている場合
このような場合は、unowned
とImplicitly 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同士の循環参照の時と同様に、weak
がunowned
で対処してあげれば良い。
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を宣言してあげれば良いです。