はじめに
UIAlertControllerは気をつけないと使い方によって循環参照します。
今回はUIAlertControllerが循環参照してしまう理由を理解するために、プログラムを作って動作確認を行いました。その結果を図解でまとめました。
関連記事
[図解]SwiftでARCのメモリ解放の仕組みを理解するために実施したこと(weak版)
[図解]SwiftでARCのメモリ解放の仕組みを理解するために実施したこと(unowned版)
環境
Xcode 10.1
Swift 4.2
作成したプロジェクト一式
https://github.com/sakamotoyuya/proj3
本プロジェクトは以下のソースコードの循環参照を確認するためのプロジェクトとなっています。
ダイアログを表示するとメモリリークが発生します。
循環参照(メモリリーク)するソースコード
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の動作
上記の結果から(4)でaddActionメソッドを呼ぶことで循環参照が発生してしまうことがわかります。
ちなみに、UIAlertControllerがactionsプロパティを持つことはAPIで公開されているので明確ですが、UIAlertActionがクロージャーをプロパティに持っているかどうかはブラックボックスのため不明確です。
なのでUIAlertControllerがクロージャーを持つと判断した根拠について説明します。
UIAlertControllerがクロージャーをプロパティに持つと判断した根拠について
アラートアクションボタンをタップ時のシーケンスを作成してまとめました。
循環参照の回避方法
上記の結果から循環参照を回避するには、考えられる方法で3つあります。(2)の箇所をキャプチャリストを用いてweakまたはunownedで参照させるか、使用済みのacを最後nilにする方法です。
修正例は以下の通り。
//(2)UIAlertActionを生成する
let action = UIAlertAction(title: "OK",style: .default){[weak ac](_)in
print(ac)
}
//(2)UIAlertActionを生成する
let action = UIAlertAction(title: "OK",style: .default){[unowned ac](_)in
print(ac)
}
//(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を実装するなどして色々試してみた結果をまとめたのが今回の記事となっています。
なかなか苦労しましたが、今回の検証を通して納得いくものになったと感じております。