クロージャでメモリリークする原因と解決策(Swift)

## はじめに

本記事は [Swift/Kotlin愛好会 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/love_swift_kotlin) の4日目の記事です。
昨日は @n_takehata さんで [【Kotlin初心者向け】KotlinでvarとMutableListを使わなくする方法](https://blog.takehata-engineer.com/entry/not-use-var-and-mutable-list) でした。

Swiftのクロージャでメモリリークする原因と解決策を紹介します。

## 環境

- OS：macOS Big Sur 11.6
- Swift：5.5
- Xcode：13.0 (13A233)

## 現象

どのような場合にクロージャでメモリリークが発生するか紹介します。

URLから画像を取得し、キャッシュした上でcompletion handlerに画像を渡す `ImageCacheManager#cacheImage(imageUrl:completion:)` というメソッドがあるとします。

```swift:ImageCacheManager.swift
final class ImageCacheManager {
    func cacheImage(imageUrl: URL, completion: @escaping (Result<UIImage, Error>) -> Void) { ... }
}
```

上記のメソッドをViewControllerから呼び出す例です。

```swift:MonsterDetailViewController.swift
final class MonsterDetailViewController: UIViewController {
    var imageCacheManager = ImageCacheManager()
    var monster: Monster!

    @IBOutlet private weak var iconImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        configureIconImageView()
    }

    private func configureIconImageView() {
        imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { result in
            switch result {
            case let .success(icon):
                DispatchQueue.main.async {
                    self.iconImageView.image = icon
                }
            case let .failure(error):
                print(error)
            }
        }
    }
```

一見問題ないコードに見えますが、実際に問題ありませんｗ
ただ他のクラスとの組み合わせによっては、メモリリークする可能性があると思います。

実務でありそう、かつ必ずメモリリークする例が作れませんでした…。

以下のサンプルコードはクロージャをプロパティに保持していないですが、必ずメモリリークします。
実行しても `deinit` の `print(_:)` メソッドが呼ばれないので、解放されていないことがわかります。

https://swiftfiddle.com/?c=H4sIAAAAAAAAA42RvWrDMBSFX%2BWgyYZW0CGLIaXQ0qkZQkonLbKkpKL6MZZUCMbvXslO7AY6VBokru8953zyQARpiLad7yNefXKSR%2B0dy1sYHgIOqX2eLkOpAcfkBAQ35t2%2FKf5VCW87o8pMgycVBO%2B0O6Gqcf%2BID69lXQaR14sOHY%2Fic59UUtRy7SgP5yx2%2BQ6sUlU918ZyjLOxVNrpeO3ueu1ixcjJe4n2rBBSy0h96R%2FX%2FLtstAAAWb%2B0YruAzV6l7rhVDQ6xLwRbMFJmGfmbe%2BHKanStY8AVZ44YlDnSonyDhH8x2cn%2FBoo56VeS0pCTLowzy%2FS2v6PmWXJHvvOP3tANfSDjD7GkuwD2AQAA

実際にメモリリークしているかは、XcodeのDebug Memory Graphを使うとわかりやすいです。

https://dev.classmethod.jp/articles/ios-memory-leak-check-and-prevent-190508/

## 結論

クロージャでメモリリークする原因と解決策を一言で紹介します。

- 原因: クロージャと `self` が循環参照しているため
- 解決策: `@escaping` なクロージャで `self` を使う場合は常に弱参照する

## 原因

原因を詳細に説明します。

### 「escaping」属性とは？

まず `@escaping` 属性について説明します。
関数の引数として渡すクロージャに `@escaping` を付けると、そのクロージャが関数のスコープ外で保持できるようになります。
関数からエスケープするので「escaping」と命名されたのだと思います。

`@escaping` を付けると「エスケープできる」ようになるのであり、必ずしもエスケープされるとは限らないので「escapable」のほうが直感的だと思いました。
しかし `@escaping` が付いているのにエスケープされないことは考えにくいので「escaping」でよさそうです。

以下に簡単な例を示します。

```swift:ImageCacheManager.swift
final class ImageCacheManager {
    private var completionHandler: ((Result<UIImage, Error>) -> Void)? = nil

    func cacheImage(imageUrl: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
        completionHandler = completion // `@escaping` が付いているので関数のスコープ外で保持できる
    }
}
```

上記の例で `@escaping` を付けないと以下のエラーが発生します。

```
Assigning non-escaping parameter 'completion' to an @escaping closure
Parameter 'completion' is implicitly non-escaping
```

![スクリーンショット 2021-11-26 0.43.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/b55d6e69-f372-31ae-89fc-ed8de02600e9.png)

~~ただクロージャを直接プロパティに代入することは少なく、実際はもう少し複雑なケースで保持することが多いです。~~
私が少なかっただけで、RxSwiftやCombineなどのライブラリを使っていたり、ViewControllerにクロージャを渡すアーキテクチャを採用していたりすると、クロージャをプロパティに保持するケースは少なくありませんでした。

関数内の非同期で実行される別のクロージャで引数のクロージャを実行するとき、エスケープしないと関数を抜けたときに別のクロージャ内で引数のクロージャを実行できません（多分）。

以下の例だと `URLSession.shared.dataTask` と `DispatchQueue.main.async` のクロージャ内で引数のクロージャを実行するのに `@escaping` が必要です。

```swift:ImageCacheManager.swift
final class ImageCacheManager {
    func cacheImage(imageUrl: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
        var imageToCache = UIImage()

        URLSession.shared.dataTask(with: imageUrl) { data, _, error in
            guard let data = data, let image = UIImage(data: data) else {
                completion(.failure(ImageCacheError.loadingFailure)) // `@escaping` が付いているので関数のスコープ外で実行できる
                return
            }

            imageToCache = image
            ImageCacheManager.imageCache.setObject(imageToCache, forKey: imageUrl as AnyObject)
            DispatchQueue.main.async {
                completion(.success(imageToCache)) // `@escaping` が付いているので関数のスコープ外で実行できる
            }
        }.resume()
    }
}
```

クロージャを不要に保持するとメモリリークに繋がるので、 __必要でない限りは `@escaping` を付けるべきではありません__ 。
Xcodeに怒られたら付けるのがいいと思います。

「クロージャに `@escaping` が付いている → このクロージャは関数のスコープ外で保持されているんだな」とわかるのが望ましいです。
関数のスコープ外で保持されていないクロージャに `@escaping` が付いていると、読み手に混乱を招きます。

### escapingなクロージャでインスタンス自身のプロパティやメソッドを呼ぶ

`@escaping` なクロージャでインスタンス自身のプロパティやメソッドを呼ぶ場合、明示的に `self.` を付けないとビルドエラーになります。

```swift:MonsterDetailViewController.swift
final class MonsterDetailViewController: UIViewController {

    private func configureIconImageView() {
        imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { result in
            switch result {
            case let .success(icon):
                DispatchQueue.main.async {
                    iconImageView.image = icon // FIXME: `self.` を付けていないのでビルドエラー
                }
            case let .failure(error):
                print(error)
            }
        }
    }

}
```

```
Reference to property 'iconImageView' in closure requires explicit use of 'self' to make capture semantics explicit

Reference 'self.' explicitly

Capture 'self' explicitly to enable implicit 'self' in this closure
```

![スクリーンショット 2021-12-03 22.33.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/381b74ed-60a9-5bd0-e449-94ab5f0dd826.png)

エラーを解消するため `self.` を戻します。

```diff_swift:MonsterDetailViewController.swift
final class MonsterDetailViewController: UIViewController {

    private func configureIconImageView() {
        imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { result in
            switch result {
            case let .success(icon):
                DispatchQueue.main.async {
-                   iconImageView.image = icon
+                   self.iconImageView.image = icon
                }
            case let .failure(error):
                print(error)
            }
        }
    }

}
```

これでクロージャが `self` を強参照します。
回り回って `self` がクロージャを保持すると循環参照するため、メモリリークするというわけです。

ちなみにどう回り回るのかはまだ理解していません…。

### escapingなクロージャでselfが省略できない理由

`self` キーワードが省略できない理由は、クロージャと `self` が循環参照するのに気づきやすくするためです。

「 `self` を付けてプロパティやメソッドを呼び出している → `@escaping` なクロージャなんだな → このクロージャは関数のスコープ外で保持されているんだな」とわかるのが望ましいです。
そのため __必要でない限りは `self` を省略する__ のがオススメです。

## 解決策

解決策を詳細に説明します。

結論で述べた通り `@escaping` なクロージャ内で `self` を強参照するのではなく、弱参照すれば循環参照しなくなり、メモリリークが発生しません。

クロージャ内の先頭で `[weak self]` を付ける（「キャプチャ」と呼ぶ）ことで、 `self` を弱参照できます。
`?` 型になり、アンラップする必要があるのに注意です。

```diff_swift:MonsterDetailViewController.swift
final class MonsterDetailViewController: UIViewController {
    var imageCacheManager = ImageCacheManager()
    var monster: Monster!

    @IBOutlet private weak var iconImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        configureIconImageView()
    }

    private func configureIconImageView() {
-       imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { result in
+       imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { [weak self] result in
            switch result {
            case let .success(icon):
                DispatchQueue.main.async {
-                   self.iconImageView.image = icon
+                   self?.iconImageView.image = icon
                }
            case let .failure(error):
                print(error)
            }
        }
    }
```

上記は `self` の参照が少ないのでオプショナルチェイニングで楽してアンラップしていますが、 `self` が `nil` になるのは異常系なので `guard let` でエラーハンドリングするのが望ましいです。

https://twitter.com/griffin_stewie/status/1463516139330306057?s=20

https://twitter.com/koher/status/1463673938286821376?s=20

あと一度 `self` を弱参照したら、下位のクロージャにも伝播します。
上記の例で `DispatchQueue.main.async { ... }` は `[weak self]` を省略していますが、付いているのと同じです。
ただ外側で `guard let self = self` していると必要になるので、常に `[weak self]` を省略しないのもありだと思います。

https://twitter.com/marty_suzuki/status/1463474602814832642?s=20

```diff_swift:MonsterDetailViewController.swift
final class MonsterDetailViewController: UIViewController {
    var imageCacheManager = ImageCacheManager()
    var monster: Monster!

    @IBOutlet private weak var iconImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        configureIconImageView()
    }

    private func configureIconImageView() {
        imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { [weak self] result in
+           guard let self = self else {
+               // TODO: エラーハンドリング
+           }
            switch result {
            case let .success(icon):
-               DispatchQueue.main.async {
+               DispatchQueue.main.async { [weak self] // !!!: `self` をアンラップしたので再度 `[weak self]` を付けないとメモリリークに繋がる
                    self?.iconImageView.image = icon
                }
            case let .failure(error):
                print(error)
            }
        }
    }
```

ちなみに `[unowned self]` でキャプチャすると `!` 型になります。
`self` が `nil` のときにクラッシュするので使いどころは難しいですが、手動でアンラップする手間が省けるので、このあたりのライフサイクルを理解していれば使っていいと思います。

https://twitter.com/noppefoxwolf/status/1463443968855662599?s=20

`weak` と `unowned` の使い分けは、以下のスライドが参考になります。

https://speakerdeck.com/yutailang0119/weak-vs-unowned

`[weak self]` を付けなくてもメモリリークしないパターンもあります。
しかし判断が難しいので、常に付けるのがベターです。

https://twitter.com/k_katsumi/status/1463342678779654149?s=20

https://twitter.com/k_katsumi/status/1463436809195511812?s=20

最終的には「 `self` を付けてプロパティやメソッドを呼び出している → `@escaping` なクロージャなんだな → このクロージャは関数のスコープ外で保持されているんだな → 循環参照を避けるために `[weak self]` を付けて弱参照しよう」に辿り着きます。

## おまけ: Swift Concurrency時代のweak self

私はまだSwift Concurrencyをあまり理解していないのですが、さらに考えることが増えるようです。

https://twitter.com/tarunon/status/1463444760589660161?s=20

https://twitter.com/tarunon/status/1463448545244368901?s=20

https://twitter.com/koher/status/1463671543955742723?s=20

## おわりに

これでもうクロージャでメモリリークすることがなくなるはずです :relaxed: 

以上 [Swift/Kotlin愛好会 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/love_swift_kotlin) の4日目の記事でした。
明日は @jollyjoester さんで「何か書く」です。

## 参考リンク

- Swift実践入門 第3版 p.142-143, 301-302
- [Automatic Reference Counting — The Swift Programming Language (Swift 5.5)](https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html)
- [Closures — The Swift Programming Language (Swift 5.5)](https://docs.swift.org/swift-book/LanguageGuide/Closures.html#:~:text=Escaping%20Closures)
- [iOSアプリのメモリリークを発見、改善する技術 - クックパッド開発者ブログ](https://techlife.cookpad.com/entry/2020/03/03/115000)
- [Add [weak self] by uhooi · Pull Request #258 · uhooi/UhooiPicBook](https://github.com/uhooi/UhooiPicBook/pull/258)
- [Remove unnecessary [weak self] by uhooi · Pull Request #259 · uhooi/UhooiPicBook](https://github.com/uhooi/UhooiPicBook/pull/259)
- https://twitter.com/the_uhooi/status/1463354028604080128?s=20
- https://twitter.com/___freddi___/status/1463336122713444353?s=20
- https://twitter.com/omochimetaru/status/1463445649727180802?s=20
- https://twitter.com/_take_hito_/status/1463468355919040512?s=20
- https://twitter.com/marty_suzuki/status/1463479022369718272?s=20
- https://twitter.com/the_uhooi/status/1463511587235401736?s=20
- https://twitter.com/the_uhooi/status/1463785352632565762?s=20
- https://twitter.com/the_uhooi/status/1463798572499931138?s=20
- https://twitter.com/_take_hito_/status/1463822403511848975?s=20
- https://twitter.com/hicka04/status/1463640655561846785?s=20
- https://twitter.com/inamiy/status/1464063488855199751?s=20
- https://twitter.com/the_uhooi/status/1466784442731872261?s=20