【翻訳】Everything you need to know about Memory Leaks in iOS

2023/07/26に公開

ARC と iOS のメモリー・リークについて知っていれば、セクション2から始めることができます。この記事はとても長いので、今回はこのセクションから見ていくことにします

  1. iOS のメモリリークとは何か

  2. なぜメモリリークが起こるのか

  3. ARC がメモリを解放できない仕組み

  4. クロージャのメモリリーク

  5. 可能な解決策

  6. 特殊なシナリオ(シングルトンと静的クラスのメモリリーク)

  7. エスケープされないクロージャ

  8. weak と unowned の違い

  9. メモリグラフデバッガを使ったリークの特定

  10. いくつかのルール

Swift は、アプリのメモリ使用量を追跡し、管理するために自動参照カウント(ARC)を使用します。ほとんどの場合、これはメモリ管理が Swift で "単に動作する "ことを意味し、メモリ管理について自分で考える必要はありません。 ARC は、クラスインスタンスによって使用されたメモリが不要になった時に、自動的に解放します。

セクション1

iOSにおけるメモリー・リーク

メモリ・リークとは、 ARC(Automatic Reference Count:自動参照カウント) がそのメモリ領域が実際に使用されているかどうかを判断できず、回復できない場合に発生します。 iOS でメモリ・リークを発生させる最も一般的な問題の1つは、後で説明する retainated cycleです。

ARCがメモリを解放できない理由

クラスの新しいインスタンスを作成するたびに、 ARC はそのインスタンスに関する情報を保存するためのメモリチャンクを確保します。インスタンスが不要になると、 ARC はメモリを解放します。それを理解するためには、まず ARC が内部的にどのように動作しているかを見る必要があります。

全ての変数はデフォルトで strong と宣言されています。 strong でクラスのインスタンスを作成すると、そのインスタンスの参照カウントがインクリメントされます。参照カウントは、そのインスタンスへの強い参照の数です。 ARC は、参照カウントが0になった変数を見つけると、そのインスタンス・メモリのメモリを解放します。

var referenceOne: Person? = Person()

上記のように、 Person オブジェクトのインスタンスを作成します。この変数には Person オブジェクト のインスタンスへの強い参照が格納されているため、 ARC はメモリを確保し、 referenceOne 変数 への参照を格納します。 ARC は図1に示すように、参照カウントを1にインクリメントします。

図1

var reference2 = referenceOne

上記のステートメントを実行した後、 Person オブジェクト・インスタンス のアドレスを reference2 変数 に代入し、別の強い参照を作成します。図2に示すように、 Person オブジェクト にもう1つの強い参照を代入した後、参照カウントは2になっています。

図2

referenceOne = nil

上記のステートメントを実行した後、変数 referenceOne に nil 値 を代入して Person オブジェクト への強い参照を解除しました。図3に示すように、 Person オブジェクト への強い参照を取り除いた後、参照カウントは1になりました。

図3

reference2 = nil

上記のステートメントを実行した後、変数 reference2 に nil 値 を代入して Person オブジェクト への強い参照を解除しました。図4に示すように、 Person オブジェクト への強い参照を取り除いた後、参照カウントは0になりました。

図4

参照カウントが 0 になると、 ARC は Person オブジェクトをメモリから削除します。これが ARC の動作です。

どのようにメモリがリークするのか?

ARC が Reference Count = 0 の変数を見つけるとその領域を解放する仕組みは理解できたと思いますが、もし Reference Count が0にならなければ、クラスのインスタンスが強い参照を0にするようなコードを書くことはできません。では、例を挙げてこのケースを再現してみましょう。

質問:ARCが変数RC(参照カウント)を見ようとするとき

cocoa や cocoa touch アプリケーション で作業しているときは、アプリケーションの実行スレッドの制御を放棄するときはいつでも、実行ループが実行されると仮定することです。したがって、メソッドを終了するときはいつでも、または NSObject で呼び出すことができる特定のメソッドがあるときはいつでも、現在の実行ループ(forループ)を実行します。排出されるたびに、変数に RC = 0 があるかどうかをチェックし、それをデアロックします。要するに、全ての実行ループの最後で、次のようにチェックします。

経験則:誰もオブジェクトを所有していない場合、 ARC はメモリを解放します。
オブジェクトのインスタンスへの強い参照を誰も持っていない場合、 ARC はメモリを解放します。
オブジェクトの参照カウントが0の場合、 ARC はメモリを解放します。
オブジェクトの強い参照がゼロの場合、そのオブジェクトはメモリから解放されます。

ここで、強い参照サイクル(参照カウントがゼロにならないことを意味する)がどのように偶然に作られるかの例を示します。図5に示すように、 User と Todo という2つのクラスを定義します。 User は todo プロパティ を持っており、 todo オブジェクト への強い参照を保存しています。

図5

var user: User? = User() //reference count 1 User object
var todo: Todo? = Todo() //Reference count 1 Todo Object

上記のステートメントを実行することで、 User オブジェクト と Todo オブジェクト のインスタンスが生成されます。 user 変数 は User オブジェクト のインスタンスへの強い参照を保持しているので、 ARC はメモリを確保し、 user 変数 に User オブジェクト の強い参照を格納します。図1に示すように、 user 変数 は User オブジェクト の強い参照を保持し、 todo オブジェクト は Todo オブジェクト のインスタンスを保持しています。

図6

user?.todo = todo //reference count 2 Todo Object
todo?.associatedUser = user //reference count 2 User Object

上記のステートメントを実行することで、図7に示すように以下のアクションを実行しました。

  1. まず、 Todo オブジェクト の参照カウントをインクリメントします。これにより、 Todo オブジェクト は2つの所有者を持つことになり、参照カウントは2になります。

  2. 次に、 User オブジェクト の参照カウントをインクリメントします。

図7

user = nil //reference count 1 User object
todo = nil //reference count 1 Todo object

理想的には、 Object を nil に設定したときに deinitializer が呼び出されるはずです。これらのシナリオでは、この2つの変数を nil に設定したときにどちらの deinitializer も呼び出されませんでした。 User インスタンス と Todo インスタンス 間の強い参照は残っており、これを解除することはできず、これらのオブジェクトのメモリを解放する方法はありません。これを強参照サイクルと呼びます。

解決策

この問題には2つの解決策があります。1つはプロパティを weak か unowned にすることです。弱い参照と所有されていない参照は、参照サイクルの中の1つのインスタンスが、そのインスタンスを牙城とすることなく他のインスタンスを参照することを可能にします。そして、インスタンスは強い参照サイクルを作ることなく、お互いを参照することができます。 strong と比べると、 weak は参照カウントを増やしません。

図9に示すように、 Todo クラス に associatedUser という weak プロパティ を作成しました。このプロパティに User オブジェクト を代入しても、参照カウントは増加せず、 User オブジェクト への強い参照は保持されません。

図9

var user: User? = User() //reference count 1 User object
var todo: Todo? = Todo() //reference count 1 Todo Object

上記のステートメントを実行することで、 user 変数 と todo 変数 にそれぞれ User オブジェクト と Todo オブジェクト が作成され、これらのオブジェクトへの強い参照が保持されます。

図10

user?.todo = todo //reference count 2 Todo Object
todo?.associatedUser = user //reference count 1 User object

図11に示すように、2つのインスタンスをリンクした結果、参照がどのようになったかを示します。 User は TodoObject への強い参照を持ち、参照カウントが2になるのに対し、 Todo オブジェクト は User オブジェクト への弱い参照を持ち、参照カウントは変わりません。

図11

user = nil

user を nil に設定すると、 User オブジェクト の参照カウントが減少し、0になります。

図12

ここで ARC は、全てのオブジェクトの参照カウントをチェックしている間に、 User オブジェクト の参照カウントが0であることを発見し、図13に示すように、メモリと関連する参照の所有者/強い参照を解放しました。 Todo オブジェクト の associatedUser プロパティ は nil に設定されています。 User オブジェクト は解放され、全ての強い参照所有プロパティも nil になり、 Todo オブジェクト の参照カウントも減少し、1になっています。

図13

todo = nil

Todo を nil に設定すると参照カウントが0になり、 ARC は Todo オブジェクト をメモリから解放します。私たちは、弱い参照を使って強い参照サイクルを解決し、ルール1を作りました。

ルール1.

2つのオブジェクトが双方向の関係にある場合、どちらかを weak または unowned にします。

クロージャーでのメモリーリーク

経験則
Memory Leak in Closure = self refers to → object refers to → self

クロージャは自己完結型の機能ブロックであり、コードの中で受け渡ししたり使用したりすることができます。クロージャは周囲のスコープから変数や定数を取り込みます。

変数のキャプチャ

上で述べたように、クロージャは周囲のスコープから変数や定数をキャプチャします。実際に見てみましょう。図14では、いくつかのステップを実行しました。

  1. まず、2つのローカル変数aとbを定義し、それぞれ20と30の値を持ちます。メソッド内で変数を定義すると、そのスコープ内でのみアクセスできます。

  2. 第二に、 someClosure 変数 を定義し、その変数への強い参照によってaとbを捕捉し、それらの値が解放されるのを防ぎ、それらが解放された場合にクロージャがクラッシュするのを防ぎます。

  3. someMethodThatTakeClosure メソッド が呼び出されると、クロージャが実行され、クロージャはviewDidLoad でキャプチャされたaとbの和を返します。図14に示すように、合計値は50と表示されました。

セクション2

クロージャのメモリリーク

もし、クロージャが必要とする値が self で、クロージャが controller のプロパティであった場合、クロージャの実行に必要な値は、その値への強い参照を保持します。

図15に示すように、 () -> Int 型 で controller のプロパティとしてオプションのクロージャを作成し、viewDidLoad で値を代入しています。クロージャを実行するには、aプロパティとbプロパティが必要です。また、a と b はクラスのプロパティなので、 self をキャプチャしてクラス参照もキャプチャする必要があります。これらはクロージャが必要とする唯一の外部値であり、これらの値を利用可能にしておく必要があります。

var someClosure: (() -> Int)?

まず、 self.someClosure がクロージャへの強い参照を保存していると言っています。

someClosure = { return self.a + self.b }.

ViewController と Closure はどちらも参照型なので、ARC参照カウントシステムが両方に適用されます。

上記のステートメントを実行することで、クロージャを定義し、クロージャへの強い参照を保持するクラスプロパティに割り当て、図 16 に示すようにクロージャの参照カウントを 1 に増やします。

Controller が Navigation controller にプッシュされたので、 Navigation controller は ViewController への強い参照を作成し、 controller の参照カウントを 1 に増やします。

同時に、クロージャは a プロパティと b プロパティを必要とするため、クラス参照も保持/キャプチャし、クラスの参照カウントを 2 にします。

図16

ViewController をポップすると、 navigation stack から controller が削除され、参照カウントが 1 になります。

ViewController の deinit が呼び出されるのが理想的ですが、 retain サイクル を作りました。これは、図 17 に示すように、2 つ以上のオブジェクト間で循環参照を行うと発生します。

ARC は参照カウント0を検索しても見つからないので、両方をメモリから削除することはありあせん。

図 17

解決策

片方を弱い参照、もう片方を強い参照にします。したがって、循環参照は破綻しています。

図18に示すように、クロージャは self への weak 参照 をキャプチャしています。 weak を使用しているため、 self はオプションとなり、そのため self の値を安全にアンラップするために guard を使用しています。

図18

var someClosure: (() -> Int)?
self.someClosure = { [weak self] in guard let `self` = self else { return 0 return self.a + self.b}

図19に示すように、 Navigation controller は SecondViewController への強い参照を作成し、参照カウントを1にします。 SecondViewController はクロージャを所有しており、クロージャはクラスプロパティなので、クロージャの参照カウントは1になります。しかし、クロージャはキャプチャ・リストで定義したように弱く自己をキャプチャするので、以下のようにクロージャの参照カウントは増えません。

図19

これで popViewController の後に以下のアクションが実行されます。

  1. navigation controller は ViewController を stack から削除し、参照カウントを 0 にデクリメントします。

  2. ARC は参照カウントが 0 のインスタンスを探し、ViewController のインスタンスを見つけ、メモリから削除し、関連する所有権も削除します。

  3. ViewController の強い参照と弱い参照も削除され、クロージャの参照カウントが 0 になり、最終的にメモリから削除されます。

weak と unowned との違い

unowned

weak 参照と同様に、 unowned 参照は参照先のインスタンスを保持しません(retain サイクルを解除するために使用できることを意味します)。

しかし、 weak 参照とは異なり、 unowned 参照は、もう一方のインスタンスのライフタイムが同じか、より長い場合に使用されます。

つまり、クロージャが実行されたときに self が利用可能であることが確実で、そうでなければアプリケーションがクラッシュしてしまうということです。あるいは、 self の方が closure よりライフタイムが長い。

Download the starter project
https://github.com/aliakhtar49/MemoryLeaks

Run application >> it will display screen having button >> Tap on Button it will Push SecondViewController >>

ViewDidLoad では someMethodThatTakeClosure が実行され、4秒かかります。4秒後に self を必要とするクロージャを実行するので、4秒前に secondviewController をポップすると、図20に示すようにアプリケーションがクラッシュします。

図20

修正

図21のように weak self を使えば、 self をオプショナルにし、 guard を使えば安全に self をアンラップできます。 self がすでに deallocated されている場合は、適切に処理できます。

図21

経験則

クロージャ内のキャプチャー・セルフが使用可能かどうか確信が持てない場合は、 weak を使用してください。

クロージャの実行時に self が利用可能であることが100%確実な場合は、 unowned を使います。

ルール2:

クラス・レベルのクロージャーと内部クロージャーがある場合、 self を使って何かにアクセスするときは、 self をweak /unowned にします。

メモリリークが発生しないケース 1

自己をキャプチャするローカル・メソッドのクロージャは、メモリ・リークを発生させません。

図22に示すように、クロージャはコントローラのクラスプロパティではないので、コントローラはクロージャへの強い参照を保持しませんが、クロージャは自己を強くキャプチャします。この場合、クロージャの寿命は常に長くなります。

図22

図23に示すように:

  1. navigation controller は SecondViewController への強い参照を保持し、その参照カウントを1にします。

  2. クロージャは self をキャプチャし、 SecondViewController インスタンスの RF = 2 を強くします。

  3. newSomeClosure 変数 は、RF = 1 にするクロージャへの強い参照を保持します。

図23

popviewController を実行すると、 Gif 1 に示すように以下のことが起こります。

  1. SecondViewController RF = 1

  2. クロージャが実行され、それを保持するローカルスコープがスタックから強く取り除かれた時、クロージャのRFは1減って0になります。

  3. クロージャRF = 0は全ての所有権を削除し、 SecondViewController RF = 0 となり、最終的にViewController をメモリから解放します。

メモリー・リークなし ケース2

self をキャプチャする static メソッド は、メモリー・リークを発生させません。

図24に示すように、 self は static クラス を所有しているのではなく、 static クラス のクロージャが self を強く捕捉しています。

静的クラス → クロージャ → self (メモリ・リークは発生しない)

以下のコードは、 DispatchQueue を所有していないため、メモリをリークすることはありません。

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.execute()
}

メモリ・リークなし ケース3:エスケープしないクロージャ

クロージャ内で self への参照を持つクラスは、参照サイクルを防ぐために、非所有の self または weak self への参照とすべきです。エスケープしないクロージャは self への参照を必要としないことに注意。

非エスケープ・クロージャは、渡されたクロージャが関数本体内で実行されることをコンパイラに伝えるので、weak self を使う必要はありません。

メモリーリーク

経験則

クロージャを保持するシングルトン/スタティック・クラスにアクセスする場合、クロージャ・メモリを保持している時間は、この時間内にリークします。

図25に示すように、 someSingletonMethod にクロージャを保持するシングルトンクラスがあり、クロージャを実行した後、そのメソッドのスコープは終了し、クロージャを強く保持するローカル変数が削除されるため、クロージャはメモリから解放されます。

SingletonClass.shared.someSingletonMethod(self.a) { (value) in
self.execute()
}

図25

図26に示すように、 someSingletonMemoryLeakMethod でクロージャを保持するシングルトンクラスがあり、実行時にクラスレベルのプロパティにそのクロージャの強い参照を保持し、クロージャを実行した後、それを解放しません。シングルトンクラスは常にメモリに残り、最終的にメモリリークを発生させるクロージャを保持するからです。この場合、メモリ・リークが発生します。

Memory Leak = SingletonClass → Closure → Self

SingletonClass が Memory に残るまで、 self(ViewController) は Memory に残ります。

クロージャはシングルトンクラスから解放されていない、あるいは保存されている controller を強く保持したままなので、ポップアップを実行しても controller は解放されません。

SingletonClass.shared.someSingletonMemoryLeakMethod(self.a) { (value) in
self.execute()
}

図26

解決策

メモリ・リークを避けるために弱い self を使います。

No Memory Leak = SingletonClass → Closure

クロージャは weak self を使用しているため、 SingletonClass はクロージャの参照のみを保持します。 controller をポップする場合、 controllerのオーナーは navigation しかないため、 controller は deallocate されます。

このシナリオはスタティック・クラスでも同じです。

メモリーリークを気にする必要はありません。

高次関数リークメモリー

高次関数が強い self を使用するため、メモリリークの可能性があります。

array.filter { [weak self] item in return self?.byTime(item) }.

プロパティ・オブザーバー

didSet はクロージャではないので、クロージャ構文を使うことはできません。

メソッドがオーナーシップサイクルを作らないのと同じように、 didSet ハンドラーはオーナーシップサイクルを作りません。

なぜなら didSet は何も捕捉せず、決して retain サイクル を作らないからです。[weak self]を使うのはナンセンスです。

メモリー・グラフ・デバッガーを使ってメモリー・リークを特定する

このパートでは、メモリ・リークの特定と、これまでのテクニックを使った修正のみを行います。主な目的は、メモリー・グラフ・デバッガーを使って既存のプロジェクトのリークを特定することです。

スターター・プロジェクトをダウンロードします。
https://github.com/aliakhtar49/MemoryLeaks

Memory Graph Debugger

一言で言えば、メモリーグラフデバッガーは以下の質問に答えるのに役立ちます。なぜオブジェクトはメモリ上に存在するのか?

Xcode のメモリグラフデバッガは、保持サイクルやリークメモリを見つけ、修正するのに役立ちます。起動すると、アプリの実行を一時停止し、現在ヒープ上にあるオブジェクトを、それらの関係やどの参照がそれらを生かしているかと共に表示します。

スタータープロジェクトを開き、アプリケーションを実行します。 "Third View Controller Title" というタイトルが表示されるまでタップし、 "Second View Controller" と表示されたら、タップして戻ってください。戻るをタップした後、 Third View Controller でメモリがリークしているかどうかを確認する必要があります。

図27に示すように、我々は今 SecondViewController にいますが、 ThirdViewController のメモリはまだ残っています。これをタップすると、クロージャが強く保持されていることが分かります。

図27

図28に示すように ViewDidLoad で発見し、図29で修正しました。

図28

図29

もう一度アプリケーションを実行し、同じ手順を踏めばメモリリークは発生しません。このように、 heap には ThirdViewController は存在しません。

図30

全てのメモリーリークを取り除いたので満足ですが、それでもこの controller でいくつかのイベントが発生するとメモリーリークが発生します。

アプリケーションをもう一度実行し、 ThirdViewController に移動してボタン1をタップします。図31に示すように、このアクションの内部でメモリリークが発生しています。

図31

要するに:

メモリリークを見つけるには、 controller が実行する全てのフローを実行し、メモリ内のグラフデバッガーの heap をチェックするか、 deinit で何かを表示する必要があります。

有益なリンク

https://medium.com/@stremsdoerfer/understanding-memory-leaks-in-closures-48207214cba

http://angelolloqui.com/blog/21-ARC-I-Introduction-to-ARC-and-how-it-works-internally

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID51

https://marcosantadev.com/capturing-values-swift-closures/

https://stackoverflow.com/questions/51429037/potential-memory-leak-using-high-order-swift-functions

https://stackoverflow.com/questions/43693703/swift-weak-self-in-didset

【翻訳元の記事】

Everything you need to know about Memory Leaks in iOS
https://medium.com/@ali-akhtar/all-about-memory-leaks-in-ios-cdd450d0cc34

Discussion