Swift の参照サイクル

Mar 14 2023
経験豊富な開発者の中にはこのような間違いを犯す人もいます
開発者にとってメモリ管理は通常、簡単なことではありません。自動参照カウント (ARC) のおかげで、iOS 開発者にとってメモリ管理が簡単になります。
著者による図、2 つ以上のオブジェクト間の参照サイクル

開発者にとってメモリ管理は通常、簡単なことではありません。自動参照カウント(ARC)のおかげで、iOS 開発者にとってメモリ管理が簡単になります。ほとんどの場合、それだけで機能します。ただし、参照サイクルは非常に扱いが難しく、多くの経験豊富な開発者がそのような間違いを犯すのを見てきました。

公式ドキュメントでは主に 2 つのオブジェクト A<->B 間の基本的な参照サイクルについて説明しています。この記事で紹介するのは、3 つまたは 4 つのオブジェクト間の参照サイクルですが、これを見つけるのはさらに困難です。

メモリリークのあるアプリ

著者によるシンプルなデモ用アプリ

デモは 2 つの画面を持つ非常にシンプルな iOS プロジェクトです。最初の画面「InitialViewController」で、ユーザーはボタンをクリックして 2 番目の画面「SampleViewController」を起動し、閉じるボタンをクリックして 2 番目の画面を閉じることができます。2 番目の画面は、VIP クリーン アーキテクチャを使用してコーディングされています。

ユーザーがアプリを楽しんでいる一方で、メモリのリークが発生しています。以下のXcodeの「Debug Memory Graph」機能の画面では、「SampleViewController」、「SamplePresenter」、「SampleInteractor」、「SampleRouter」のインスタンスがそれぞれ5つずつリークしていることがわかります。メモリリークが多すぎると、アプリはシステムによって強制終了され、アプリ開発者にとっては非常に恥ずかしいことになります。

Xcode の作成者によるスクリーン キャップ、デバッグ メモリ グラフ

複数のオブジェクト内の参照サイクル

以下のソースコードを詳しく見て、メモリリークが発生する理由を調べてみましょう。

class SampleViewController: UIViewController, SampleViewControllerInterface {
    var interactor: SampleInteractorInterface?
    // omitted the rest for simplicity
}

class SampleInteractor: SampleInteractorInterface {
    var presenter: SamplePresenterInterface?
    // omitted the rest for simplicity
}

class SamplePresenter: SamplePresenterInterface {
    var viewController: SampleViewControllerInterface?
    var router: SampleRouterInterface?
    // omitted the rest for simplicity
}

class SampleRouter: SampleRouterInterface {
    var viewController: UIViewController?
    // omitted the rest for simplicity
}

作成者作成の図、オブジェクト間の参照関係

実際には 2 つの参照サイクルがあります。

  1. インタラクター -> プレゼンター -> ViewController -> インタラクター
  2. インタラクター -> プレゼンター -> ルーター -> ViewController -> インタラクター

これを解決するには、以下の変更のように弱い参照を使用できます。

作者別のスクリーンキャップ、VIP の参照サイクルを修正する方法

次に、次の図に示すように、VIP には参照サイクルがなくなりました。

作者作成の図、修正後のオブジェクト間の参照関係

ブロックキャプチャに関する基準周期

ほー!2 つの参照サイクルを修正しただけなので、とても良い感じですが、まだ終わっていません。ここでアプリを実行すると、メモリ リークがまだ発生していることがわかります。原因はコードの以下の部分です。

class CustomButton: UIView {
    var buttonClickHander: (() -> Void)?
    // omitted the rest for simplicity
}

class SampleViewController: UIViewController, SampleViewControllerInterface {
   private lazy var customButton: CustomButton = {
        let button = CustomButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    private func setUpUI() {
        view.addSubview(customButton)
        customButton.buttonClickHander = {
            self.interactor?.customButtonClicked()
        }
        // ... 
    }
}

作成者作成の図、オブジェクトとブロックの参照関係

問題の場所がわかれば、修正は簡単です。以下を参照してください。

作者によるスクリーンキャップ、ブロックキャプチャの参照サイクルを修正する方法

まとめ

要約すると、複数のオブジェクト内で発生する 2 種類の参照サイクルについて説明しました。デモ プロジェクトはここで見つけることができます。修正は簡単にレビューできるようにプル リクエストとして用意されています。この記事が参照サイクルをより深く理解し、自信を持って参照サイクルを回避するのに役立つことを願っています。