「メモリリークが発生していることはわかるが、その原因がわからない。」
このような質問を、聞くことが多々あります。
行き詰まる方の助けになれればと思い、
今回、そのポイントをまとめてみました。
知ってる方は「あるある!!」と思っていただければ幸いです。
メモリリークって何?
何かが邪魔をして、画面を閉じてもインスタンスが残ったまま消えないという現象。
これでは、メモリが解放されません。
つまり、これを繰り返していると どんどん動作が重くなり、最悪の場合 アプリが落ちます。
それだけは避けたいですね。
ということで…
早速、見落としやすいポイントを見てみましょう。
メモリリークのパターン・解消
メモリリークの発生箇所は、決まって2つ。
-
クロージャー (
closure
) -
デリゲート(
delegate
)
では、何が原因?
全て 強参照 が原因です。
1. クロージャー (closure)
クロージャー とは、処理(関数)そのものを「値」として 変数に格納できる もの。
引数として使えば 、特定の処理を設定して…呼び出した後に実行したり。
とにかく便利なんです。彼。
ただ、そのままにしておくとメモリリークを引き起こします。
ご安心ください。弱参照 [weak self]
で全て解決します。
class TestClass {
var text = "test"
lazy var testClosure: (() -> ()) = { [weak self] in // <- ここ
print(self?.text)
}
}
クロージャー を生成した際、クラス内の変数・関数を呼び出していないか( self
を普通に、または暗黙的に使っていないか)をチェックしてください。
もし使っていたら、[weak self]
を使いましょう。
別クラスからクロージャーをセット
TestClass2 という別のクラスから、testClass にクロージャーを変数にセットします。
class TestClass2 {
var text = "test"
var testClass = TestClass()
func setClosure() {
testClass.testClosure = { [weak self] in
print(self?.text)
}
}
}
この場合、残念ながら 弱参照 [weak self]
だけでは問題は解決しません。
別クラスのインスタンスが絡んでしまって、自動ではインスタンスが解放されなくなります。
結果、TestClass が解放されません。(TestClass2 は解放される)
インスタンスを解放する前に、
処理を書いて、クロージャーを解放する必要があります。
class TestClass {
var text = "test"
var testClosure: (() -> ())?
...
}
testClosure = nil
DispatchQueue
DispatchQueue
や UIView.animate
等の場合は、基本的には不要です。
ただ、メモリリーク以外の問題があります。
実行時点で既に self となるインスタンスが解放済みである場合、落ちます。(nilを強参照したら…そりゃね)
危なさそうな場所には、つけておいた方が無難ですね。
DispatchQueue.main.async { [weak self] in // <- ココ
self?.image = image
}
要は、一瞬で終わらないような非同期処理のあとに、DispatchQueueを使ってるケース。
「Web経由の非同期処理の後に、UIを変更する」…といった場合に必要です。
(その場合、DispatchQueue より外の問題だけど)
危なさそうな場所には、つけておいた方が無難ですね。
guard let
でもっとシンプルに
testClosure = [weak self] in
guard let `self` = self else { return }
// 中の処理
print(self.text)
}
普段通りの書き方ができるほか、
クラス自体がすでに解放されている場合は、処理を安全にキャンセルします。
2. デリゲート (delegate)
外側にあるクラスから処理を呼び出したい時に使う、アレですね。
その点は、もうご存知かと思います。
こちらも強参照によるもの。
デリゲートを自作する場合、そのままだとメモリリークが発生します。
ここでの ポイントは2つ です。
- delegate を格納する場合、弱参照
weak
にする。 - delegate の
protocol
は、AnyObject
を継承しておく。
class TestClass {
weak var delegate: SampleDelegate?
...
}
protocol SampleDelegate: AnyObject {
...
}
これだけ。
URLSession
URLSession
を使用する際にも注意。元々、デリゲート自体が強参照されています。
流石に、ここを弱参照にすることはできませんね。
ここでは、対応策が2つ ほどあります。
-
URLSession.shared
を使う(こちらを使っているなら、そもそも発生しない。) -
invalidateAndCancel()
等の 専用メソッド で、セッションを明示的に無効化する。
前者は URLSession.shared
が使える場合ですね。
細かい設定は使えませんが、大抵はこちらで良いかと。
後者の専用メソッドについては、こちらの記事が参考になります。
【その他】
本記事は例の紹介です。
極力、別の話題は省いています。
メモリリークの仕組みに関しては、以下の記事が参考になります。
ARC(Automatic Reference Counting) と呼ばれる機能を、swiftは有しています。
今回の問題も、全てはこれに基づいていますね。
また、
メモリリークが発生しているか、チェックしたい 場合はこちら。
- iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。 //qiita
- [iOS] メモリリークをXcodeでチェックして、リークしないようにしたい! //DevelopersIO
ということでね
メモリリークのパターン(例)をまとめました!!(パターン紹介以外はほぼ記事のリンク紹介ですが)
思い当たる内容はあげましたが、その他にもある場合は教えていただけると助かります。
原因に関する詳しい事柄は、多くの方々が記事にまとめてくれていると思います。
そのため、本記事は「メモリリークから原因につなげるための記事」として書きました。
原因さえわかれば、それらの記事にたどり着けると思います。