LoginSignup
12
13

More than 5 years have passed since last update.

[図解]SwiftでUIAlertControllerが何故循環参照するのかを理解するために実施したこと

Last updated at Posted at 2019-04-07

はじめに

UIAlertControllerは気をつけないと使い方によって循環参照します。
今回はUIAlertControllerが循環参照してしまう理由を理解するために、プログラムを作って動作確認を行いました。その結果を図解でまとめました。

関連記事

[図解]SwiftでARCのメモリ解放の仕組みを理解するために実施したこと(weak版)
[図解]SwiftでARCのメモリ解放の仕組みを理解するために実施したこと(unowned版)

環境

Xcode 10.1
Swift 4.2

作成したプロジェクト一式

https://github.com/sakamotoyuya/proj3
スクリーンショット 2019-04-08 1.07.26.png

本プロジェクトは以下のソースコードの循環参照を確認するためのプロジェクトとなっています。
ダイアログを表示するとメモリリークが発生します。

循環参照(メモリリーク)するソースコード

ViewController.swift(循環参照するコード)
    func call(){

        //(1)UIAlertControllerを生成する
        let ac = UIAlertController(title: "タイトル", message: "メッセージ", preferredStyle: .alert)

        //(2)UIAlertActionを生成する
        let action = UIAlertAction(title: "OK",style: .default){(_)in

            //(3)クロージャー内でacを参照する
            print(ac)
        }

        //(4)acのプロパティactionsにactionを追加する
        ac.addAction(action)

        //acを表示
        //今回ここは循環参照とは関係ないので図には出てきません。
        //アラートを表示する際は使うものなので一応記載しています。
        present(ac, animated: true)
    }

このcallメソッドを呼ぶとメモリリークが発生します。
それではCallメソッドが呼ばれた場合のARCの動作を見ていきます。

Callメソッドが呼ばれた場合のARCの動作

スクリーンショット 2019-04-07 23.06.41.png
スクリーンショット 2019-04-07 23.06.47.png
スクリーンショット 2019-04-07 23.06.54.png
スクリーンショット 2019-04-07 23.07.00.png
スクリーンショット 2019-04-07 23.07.07.png
スクリーンショット 2019-04-07 23.07.15.png
スクリーンショット 2019-04-07 23.07.24.png
スクリーンショット 2019-04-07 23.07.33.png

上記の結果から(4)でaddActionメソッドを呼ぶことで循環参照が発生してしまうことがわかります。
ちなみに、UIAlertControllerがactionsプロパティを持つことはAPIで公開されているので明確ですが、UIAlertActionがクロージャーをプロパティに持っているかどうかはブラックボックスのため不明確です。
なのでUIAlertControllerがクロージャーを持つと判断した根拠について説明します。

UIAlertControllerがクロージャーをプロパティに持つと判断した根拠について

アラートアクションボタンをタップ時のシーケンスを作成してまとめました。
スクリーンショット 2019-04-08 0.09.32.png
スクリーンショット 2019-04-08 0.09.43.png
スクリーンショット 2019-04-08 0.09.52.png

循環参照の回避方法

上記の結果から循環参照を回避するには、考えられる方法で3つあります。(2)の箇所をキャプチャリストを用いてweakまたはunownedで参照させるか、使用済みのacを最後nilにする方法です。
修正例は以下の通り。

(2)の箇所[修正案1]
    //(2)UIAlertActionを生成する
    let action = UIAlertAction(title: "OK",style: .default){[weak ac](_)in
        print(ac)
    }
(2)の箇所[修正案2]
    //(2)UIAlertActionを生成する
    let action = UIAlertAction(title: "OK",style: .default){[unowned ac](_)in
        print(ac)
    }
(2)の箇所[修正案3]
    //(2)UIAlertActionを生成する
    let action = UIAlertAction(title: "OK",style: .default){(_)in
        print(ac)
        ac = nil
    }

今回はどれでやってもメモリリークの発生は問題なく防げますね。
※そもそもprint(ac)の箇所でacを使わないようにするのも一つの手なのですが、今回はこれがあることを前提とした解決方法としています。

ここまでやってみて

最初はCallメソッドはローカル変数をacとactionの二つしか持っていなく、メソッド終了時にこの二つの変数のメモリは解放されるから、何も意識しなくても問題ないと思っていました。今回のように用意されているAPIの中(UIAlertControllerクラスとUIAlertActionクラス)で循環参照がおこると、メモリリークに気づくのは難しそうです。実際に理解するのには苦労しました。なので、頭ごなしに大丈夫と思うのではなく、用意されてるAPIの中でもリークすることはあるということ意識して設計を行っていくことも大切なのかもしれません。

ちなみに、UIAlertControllerは自作したクラスでないためdeinitされたことがわからないので、UIAlertControllerクラスとUIAlertActionクラスを模倣したクラスを作成してみてそのクラスにdeinitを実装するなどして色々試してみた結果をまとめたのが今回の記事となっています。
なかなか苦労しましたが、今回の検証を通して納得いくものになったと感じております。

12
13
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
12
13