35
31

More than 3 years have passed since last update.

循環参照がどういうことなのか視覚的に分かりやすく見てみる

Last updated at Posted at 2021-03-13

目次

  • はじめに
  • 前提確認
  • サンプルコードを用意
  • 強参照で繋いてみる
  • 弱参照で繋いでみる
  • まとめ

はじめに

循環参照ってよく聞きますよね。関連ワードとしては強参照とか、弱参照とか、メモリが解放されないとか…。難しい概念的なことで、結局どういうことが起こってるの?何がダメなの?といまいち腹落ちしていませんでした。そこで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")

これはjohn2johnを強参照している状態です。
johnnilを入れても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

これはjohn2johnを弱参照している状態です。
johnnilを入れるとjohn2からは弱参照しかされていないので、johnは消えてしまいます。
なのでその後にjohn2を呼び出しても参照先が消えているので、nilになってしまうんですね。

③メモリが解放されるルール

①②で見てきたように、強参照されている限り、インスタンスは消えません。なのでメモリとして残り続けます。逆に言うと、「1つでも強参照がなくなれば、そのインスタンスを消すことができ、メモリを解放できる」と言えます。このルールは覚えておいてください。

サンプルコードを用意

前提知識を確認したところで、実際に循環参照について見ていきましょう。

まずはサンプルコードを用意します。仕様としては、
CustomViewを作って+ボタンと-ボタンを用意して
MainViewController上の数字が入るLabelを増減させる
StartViewControllerを最初の画面として、MainViewControllerと行き来できるようにします

CustomViewからはpotocolを適用させたdelegate変数を用意し、delegate適用先をMainViewControllerに設定します。ここのdelegate変数の接続の仕方が今回のポイントです。

大まかにコードを見てましょう。

CustomView
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メソッド
    }


MainViewController
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)"
    }
}
StartViewController
class StartViewController: UIViewController {

    @IBAction func exitFromMainViewController(segue: UIStoryboardSegue) {
        print("StartViewControllerに戻ってきました")
    }
}

強参照で繋いでみる

では注目ポイントである以下のコードを見てましょう。

CustomView
class CustomView: UIView{

    var delegate: ButtonActionDelegate? //今回はここが注目ポイント

これは今強参照の状態ですね。この状態でアプリを起動し、StartViewControllerMainViewControllerをわざと3回行き来します。その後Xcodeの"Debug Memory Graph"を見ます。

WeChat9829f756b7b322fbbdba2cc819b56e3a.png

WeChat80352bfc5bfaf3676fb832944a4da1a0.png

すると↑のようにアプリが起動している各ファイルやそれらの参照状態を見ることができます。(ざっくり)赤枠部分を見ると、CustomView(3)とMainViewController(3)と書かれていますね。

これがどういうことかというと、実はアプリの裏でそれぞれのファイルが3つ呼ばれていて存在し続けているんです、もう使わないのに。しかもその分メモリを占領してしまいます。今回は軽い仕様だから良いものの、これに大容量の画像とかがついてたら、すぐにメモリがいっぱいになって最悪クラッシュします。

なんでこんなことになっているのか。
それはCustomViewdelegate先としてMainViewControllerを強参照していて、かつMainViewControllerCustomViewをまた強参照しているからなんですね。お互いがお互いを強参照しているので、StartViewControllerに戻ってもどっちも消えず、残り続けることになります。もう使わないのに。このようにお互いがお互いを強参照している状態を循環参照していると言います。また、こうしてファイルが残り続けメモリが解放されないことをメモリリークというようです。

 
↓CustomViewからdelegate先としてMainViewControllerを強参照している
WeChatf8df1f5ffeea4390cf621e4ea45fb708.png

↓MainViewから(間のもの含めて全て)CustomViewまで強参照している
WeChatc09a6ac1ac356c807c6783a000c18d96.png

ではどうすれば良いのか。
答えは簡単で、どちらかをweakで弱参照にすればOKです。

弱参照で繋いでみる

では、先ほど強参照にしていた箇所を弱参照にしてみます。

CustomView
class CustomView: UIView{

    weak var delegate: ButtonActionDelegate? //今回はここが注目ポイント

その後同じようにStartViewControllerMainViewControllerをわざと3回行き来します。
結果は↓のように、StartViewControllerに戻ってきたことでCustomViewMainViewControllerも消えています。

WeChat89707f8cc310c7d80d7cf7b7a8d0e198.png

これはCustomViewdelegate適用時に弱参照しているため、StartViewに戻ってきた時にMainViewControllerがどこからも強参照されないために消え、その結果CustomViewがどこからも強参照されなくなったのでCustomViewも消えているのです。

これで使わないファイルは消えて、無駄なメモリを使用しなくてすみますね。

まとめ

少し長くなってしまいましたが、こうして視覚的にみるとイメージが湧きやすかったではないでしょうか。

ここで見たものは基本的な流れで、参照の仕方についてはまだまだ奥が深いですが、そうした深い知識を学ぶためのとっかかりとなれば嬉しいです。

こうして自分で色々試してみると見えてくることがありますので、是非ご自身でも色々試してみてください!

35
31
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
35
31