[iOS] そのアプリ、メモリリークしてませんか?

2016.07.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは。モバイルアプリサービス部の加藤潤です。 今回はiOSアプリ開発において気をつけていないと発生しがちなメモリリークについて、よくある例を書いてみたいと思います。

開発環境

  • Xcode 7.3.1 (7D1014)
  • iPhone 6s シミュレータ / iOS 9.3 (13E230)
  • Swift 2.2

お互いをプロパティで強参照し合うケース

これは典型的な循環参照が発生するパターンです。
まずはXcodeで新規プロジェクトを作成します。iOS > Application > Single View Application を選択します。
プロジェクトが作成できたらNew File... > Swift Fileを選択します。ファイル名はModelAとでもしておきましょう。 同様にModelBも作成しましょう。
ファイルが作成できたら以下のようにModelAModelBという2つのクラスを作成します。

ModelA.swift

import Foundation

class ModelA {
    var modelB: ModelB?

    deinit {
        print("ModelA deinit")
    }
}

ModelB.swift

import Foundation

class ModelB {
    var modelA: ModelA?

    deinit {
        print("ModelB deinit")
    }
}

ModelAModelBを、ModelBModelAをそれぞれプロパティで保持しています。
deinitはオブジェクトが破棄される時に呼ばれるものですが、今回はオブジェクトが正しく破棄されているかを確認するためにprintしています。

それでは上記のクラスを使ってみましょう。 ViewController.swiftを以下のように変更します。

ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // ModelA, ModelBのインスタンスを生成
        let a = ModelA()
        let b = ModelB()

        // プロパティにセット
        a.modelB = b
        b.modelA = a
    }
}

変更したらアプリを実行してみましょう。
コンソールにdeinitのログは出ましたか?

出ませんか?

はい、これは出ないのが正しいんです。
理由は簡単で、ModelAModelBがお互いを強参照しあっているためです。

  • ModelAが破棄されるタイミングでプロパティに保持したModelBが破棄される
  • ModelBが破棄されるタイミングでプロパティに保持したModelAが破棄される

上記のようにお互い解放待ちの状態になってしまっています。メモリリークの完成です。

メモリリークしないようにするには

ではメモリリークを防ぐにはどうしたら良いでしょうか? メモリリークの原因はModelAModelBがお互いを強参照しあっていることにあるので、どちらか一方を弱参照にしてあげればメモリリークは発生しなくなります。 ここではModelBが保持しているModelAプロパティにweak修飾子をつけてみましょう。

ModelB.swift

import Foundation

class ModelB {
    weak var modelA: ModelA?

    deinit {
        print("ModelB deinit")
    }
}

この状態でアプリを実行してみます。
ちゃんとdeinitのログが出ました。オブジェクトが正しく破棄されていることの証明です。
メモリリークを防ぐことができました。

ModelA deinit
ModelB deinit

プロパティに保持したクロージャにselfが強参照でキャプチャされているケース

続いてはクロージャを使用したメモリリークの例です。
まずは以下のクラスを見てください。

ClosureHolder.swift

import Foundation

class ClosureHolder {
    private var myClosure: (() -> Void)?

    deinit {
        print("ClosureHolder deinit")
    }

    init() {
        myClosure = { () in
            self.innerFunc()
        }
        myClosure?()
    }

    func innerFunc() {
        print("innerFunc")
    }
}

4行目でクロージャを保持するプロパティを定義しています。
11〜14行目でクロージャの中身を定義し、すぐさまクロージャを実行しています。

上記のクラスを使ってみましょう。以下のようにClosureHolderを生成するだけです。

ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let _ = ClosureHolder()
    }
}

実行結果は以下のようになりました。

innerFunc

innerFuncが呼ばれたログは出ていますが、deinitのログが出ていません。ちゃんとClosureHolderが解放されていないんです。
理由はクロージャによってselfが強参照でキャプチャされ、そのクロージャがselfによって強参照で保持されている、つまり、クロージャとselfの間で循環参照が発生しているためです。

メモリリークしないようにするには

この場合はクロージャのキャプチャ時の参照方法を変えることでメモリリークを防ぐことができます。 具体的には以下のように[weak self]を付与することでクロージャがselfを弱参照するようにすればOKです。
また、weakにすることでselfがOptionalになるためselfの処理を呼び出す時は?を付けてアンラップする必要があります。

ClosureHolder.swift

import Foundation

class ClosureHolder {
    private var myClosure: (() -> Void)?

    deinit {
        print("ClosureHolder deinit")
    }

    init() {
        myClosure = { [weak self] () in // selfのキャプチャをweakに変更する
            self?.innerFunc() // weakにすることでselfがOptionalになるため?が必要
        }
        myClosure?()
    }

    func innerFunc() {
        print("innerFunc")
    }
}

この状態でアプリを実行すると以下のような結果になります。

innerFunc
ClosureHolder deinit

ちゃんとdeinitのログが出て、メモリリークを防ぐことができました。

まとめ

今回はメモリリークのよくある例をご紹介しました。
オブジェクトの参照方法(強参照なのか弱参照なのか)について何も考えずに実装していると、知らず知らずのうちにメモリリークが発生してしまうかもしれません。 Xcode 4.2からARC(Automatic Reference Counting)が導入されたことでだいぶ参照カウント周りの実装が楽になりましたが、メモリリークが発生しないように意識するのはプログラマの責任です。