5
6

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 2021-03-24

メモリリークが発生していることはわかるが、その原因がわからない。

このような質問を、聞くことが多々あります。

行き詰まる方の助けになれればと思い、
今回、そのポイントをまとめてみました。

知ってる方は「あるある!!」と思っていただければ幸いです。

メモリリークって何?

tumatta.png
何かが邪魔をして、画面を閉じてもインスタンスが残ったまま消えないという現象。
これでは、メモリが解放されません。

つまり、これを繰り返していると どんどん動作が重くなり、最悪の場合 アプリが落ちます

memoryLeak.png
アプリが落ちる…

それだけは避けたいですね。
ということで…
早速、見落としやすいポイントを見てみましょう。

メモリリークのパターン・解消

メモリリークの発生箇所は、決まって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

DispatchQueueUIView.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は有しています。
今回の問題も、全てはこれに基づいていますね。

cheakMemory.png

また、
メモリリークが発生しているか、チェックしたい 場合はこちら。

ということでね

メモリリークのパターン(例)をまとめました!!(パターン紹介以外はほぼ記事のリンク紹介ですが)
思い当たる内容はあげましたが、その他にもある場合は教えていただけると助かります。

原因に関する詳しい事柄は、多くの方々が記事にまとめてくれていると思います。
そのため、本記事は「メモリリークから原因につなげるための記事」として書きました。

原因さえわかれば、それらの記事にたどり着けると思います。

5
6
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
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?