目次
- はじめに
- 前提確認
- サンプルコードを用意
- 強参照で繋いてみる
- 弱参照で繋いでみる
- まとめ
はじめに
循環参照ってよく聞きますよね。関連ワードとしては強参照とか、弱参照とか、メモリが解放されないとか…。難しい概念的なことで、結局どういうことが起こってるの?何がダメなの?といまいち腹落ちしていませんでした。そこでXcodeの"Debug Memory Graph"を使ってみると、循環参照について視覚的に分かりやすかったのでまとめてみました。この記事を読むことでイメージが湧いて、他のもっと詳しい関連記事が読みやすくなるかと思います。
前提確認
MacOS Catalina 10.15.4
Xcode 12.4
Swift version 5
最初に前提として知っておきたい知識を確認します。
とりあえず深く理解する必要はなく、こういうのがあるんだなくらいで大丈夫です。
①強参照
これはよくみる一番シンプルな参照。
class Person{
let name : String
init(name : String){
self.name = name
}
}
var john :Person? = Person(name: "John")
var john2 = john //←実はこれが強参照
john = nil
print(john2?.name) //Optional("John")
これはjohn2
がjohn
を強参照している状態です。
john
にnil
を入れてもjohn2
から強参照されているので、john
は消えないんですね。
このように強参照されているインスタンスはnilが入っても残り続けます。
②弱参照
IBOutlet接続でよくみるweak
が付いている参照。
class Person{
let name : String
init(name : String){
self.name = name
}
}
var john :Person? = Person(name: "John")
weak var john2 = john //←これが弱参照
john = nil
print(john2?.name) //nil
これはjohn2
がjohn
を弱参照している状態です。
john
にnil
を入れるとjohn2
からは弱参照しかされていないので、john
は消えてしまいます。
なのでその後にjohn2
を呼び出しても参照先が消えているので、nil
になってしまうんですね。
③メモリが解放されるルール
①②で見てきたように、強参照されている限り、インスタンスは消えません。なのでメモリとして残り続けます。逆に言うと、「1つでも強参照がなくなれば、そのインスタンスを消すことができ、メモリを解放できる」と言えます。このルールは覚えておいてください。
サンプルコードを用意
前提知識を確認したところで、実際に循環参照について見ていきましょう。
まずはサンプルコードを用意します。仕様としては、
①CustomView
を作って+ボタンと-ボタンを用意して
②MainViewController
上の数字が入るLabelを増減させる
③StartViewController
を最初の画面として、MainViewController
と行き来できるようにします
CustomView
からはpotocolを適用させたdelegate
変数を用意し、delegate
適用先をMainViewController
に設定します。ここのdelegate
変数の接続の仕方が今回のポイントです。
大まかにコードを見てましょう。
import UIKit
@objc protocol ButtonActionDelegate: NSObjectProtocol { //プロトコルを用意
func plus()
func minus()
}
class CustomView: UIView{
var delegate: ButtonActionDelegate? //今回はここが注目ポイント
/*------------このからはCustomViewの初期化なので今回は関係ありません---------------*/
override init(frame: CGRect) {
super.init(frame: frame)
loadNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadNib()
}
func loadNib() {
let view = Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)?.first as! UIView
view.frame = self.bounds
self.addSubview(view)
}
/*------------ここまではCustomViewの初期化なので今回は関係ありません---------------*/
@IBAction func didTapUp(_ sender: Any) {
delegate?.plus() //delegateメソッド
}
@IBAction func didTapDown(_ sender: Any) {
delegate?.minus() //delegateメソッド
}
import UIKit
class MainViewController: UIViewController, ButtonActionDelegate { //プロトコルを適用
@IBOutlet weak var viewForCustomView: CustomView!
@IBOutlet weak var numberLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
print("MainViewControllerを開きました")
viewForCustomView.delegate = self //delegate先を自分に設定
}
private var number = 0
func plus() { //delegateメソッド実行
number += 1
numberLabel.text = "\(number)"
}
func minus() { //delegateメソッド実行
number -= 1
numberLabel.text = "\(number)"
}
}
class StartViewController: UIViewController {
@IBAction func exitFromMainViewController(segue: UIStoryboardSegue) {
print("StartViewControllerに戻ってきました")
}
}
強参照で繋いでみる
では注目ポイントである以下のコードを見てましょう。
class CustomView: UIView{
var delegate: ButtonActionDelegate? //今回はここが注目ポイント
これは今強参照の状態ですね。この状態でアプリを起動し、StartViewController
とMainViewController
をわざと3回行き来します。その後Xcodeの"Debug Memory Graph"を見ます。
すると↑のようにアプリが起動している各ファイルやそれらの参照状態を見ることができます。(ざっくり)赤枠部分を見ると、CustomView(3)とMainViewController(3)と書かれていますね。
これがどういうことかというと、実はアプリの裏でそれぞれのファイルが3つ呼ばれていて存在し続けているんです、もう使わないのに。しかもその分メモリを占領してしまいます。今回は軽い仕様だから良いものの、これに大容量の画像とかがついてたら、すぐにメモリがいっぱいになって最悪クラッシュします。
なんでこんなことになっているのか。
それはCustomView
がdelegate
先としてMainViewController
を強参照していて、かつMainViewController
がCustomView
をまた強参照しているからなんですね。お互いがお互いを強参照しているので、StartViewController
に戻ってもどっちも消えず、残り続けることになります。もう使わないのに。このようにお互いがお互いを強参照している状態を循環参照していると言います。また、こうしてファイルが残り続けメモリが解放されないことをメモリリークというようです。
↓CustomViewからdelegate先としてMainViewControllerを強参照している
↓MainViewから(間のもの含めて全て)CustomViewまで強参照している
ではどうすれば良いのか。
答えは簡単で、どちらかをweakで弱参照にすればOKです。
弱参照で繋いでみる
では、先ほど強参照にしていた箇所を弱参照にしてみます。
class CustomView: UIView{
weak var delegate: ButtonActionDelegate? //今回はここが注目ポイント
その後同じようにStartViewController
とMainViewController
をわざと3回行き来します。
結果は↓のように、StartViewController
に戻ってきたことでCustomView
もMainViewController
も消えています。
これはCustomView
がdelegate
適用時に弱参照しているため、StartView
に戻ってきた時にMainViewController
がどこからも強参照されないために消え、その結果CustomView
がどこからも強参照されなくなったのでCustomView
も消えているのです。
これで使わないファイルは消えて、無駄なメモリを使用しなくてすみますね。
まとめ
少し長くなってしまいましたが、こうして視覚的にみるとイメージが湧きやすかったではないでしょうか。
ここで見たものは基本的な流れで、参照の仕方についてはまだまだ奥が深いですが、そうした深い知識を学ぶためのとっかかりとなれば嬉しいです。
こうして自分で色々試してみると見えてくることがありますので、是非ご自身でも色々試してみてください!