17
4

More than 3 years have passed since last update.

クロージャの中に書く[weak self]についてまとめてみた

Last updated at Posted at 2021-01-26

自分の備忘録。

はじめに

ある日、メンターさんにコードレビューして頂いている時に言われた「[weak self]がないと循環参照してメモリリークする」という一言からこの記事を書こうと思いました。

結論から

色々、調べていくと[weak self]と書くことでクロージャがselfを弱参照し、クロージャとselfの循環参照を防ぐことができるそうです。

この時点では???です。

そして、新たに弱参照という単語が出てきました。
循環参照を知るには、まだ時間が掛かりそうですね。

調べてみましょう。

強参照

調べていくと、まずは弱参照と対にある強参照を知る必要がありそうですね。

強参照とはあるオブジェクトが他のオブジェクトを参照する際のデフォルトの参照方法です。

swiftではARC(Auto Reference Counting)という仕組みを用いて、オブジェクトが他のオブジェクトから参照されている数(参照カウント)を記憶してメモリ領域内のオブジェクト管理を行います。

そして、メモリに確保されたインスタンスやプロパティが必要でなくなったら
ARCによって自動的に破棄されます。
Swiftのメモリ管理【weakやunownedをわかりやすく解説】

class A {}

// 参照カウントが1になる
var a: A? = A() // 1

// 参照カウントが0になり解放される
a = nil // 0

ですが、この強参照という方法は循環参照という問題を引き起こしてしまいます。

循環参照

参照型のオブジェクト同士では強参照による循環(strong reference cycle)が起こってしまいます。

コードで表すとこんな感じ↓

class A {
    var b: B?
}

class B {
    var a: A?
}

// クラスAとBのインスタンス
var a: A? = A() // Aクラスの参照カウント1
var b: B? = B() // Bクラスの参照カウント1

// --------互いに参照し合う--------

b?.a = a // Aクラスの参照カウント2
a?.b = b // Bクラスの参照カウント2

// --------互いに参照し合う--------

// たとえ各インスタンスが解放されたとしても、それぞれのクラス内のプロパティから参照し合っているのでどちらも参照カウントは1である
a = nil // Aクラスの参照カウント1
b = nil // Bクラスの参照カウント1

これが循環参照です。

つまり、お互いに参照し合って、どちらも解放されずにそのままメモリ内に残り続けてしまうという状態です。

これらが原因でメモリリークが起きてしまうんですね。
メモリリークの説明はこちらの記事が凄く分かりやすく解説してくれていますので参考にしてみて下さい↓
Swiftのメモリリークについてまとめてみた

クロージャでも起こりうる循環参照

class A {
    private var closure: (() -> Void)?
    private var count = 0

    init() {
          closure = createClosure()
    }

    func createClosure() -> (() -> Void) {
      return { [self] in self.count += 1 }
    }
}

var a: A? = A() // Aクラスの参照カウント2、closureの参照カウント1
a = nil // Aクラスの参照カウント1

// selfとクロージャが参照し合っているので循環参照が起こってしまっている

Aクラスを生成した段階で、Aクラスのイニシャライザ内部でcreateClosure()メソッドが呼び出されて
{ [self] in self.count += 1 }のインスタンスが生成されます。

そして、closureプロパティに代入され

self(Aクラス) > クロージャ({ [self] in self.count += 1 })への参照

が発生。

更に、このクロージャは内部でselfを参照しているので

クロージャ({ [self] in self.count += 1 }) > self(Aクラス)への参照

も発生してしまう。

そうなると、たとえaインスタンスが解放されたとしてもself(Aクラス)クロージャ({ [self] in self.count += 1 })がお互いに参照し合うので参照の循環が起きてしまい循環参照の原因となる訳です。

弱参照

この循環参照を解決するのが弱参照(weak reference)です。

これはARCの参照カウントには含まれず、他のオブジェクトから参照されながらであってもメモリを解放することができるという参照方法です。

じゃあ、どうやって弱参照するのか?というと[weak self]をクロージャ内に記述します。

class A {
    private var closure: (() -> Void)?
    private var count = 0

    init() {
          closure = createClosure()
    }

    func createClosure() -> (() -> Void) {
      return { [weak self] in self?.count += 1 }
    }
}

var a: A? = A() // Aクラスの参照カウント1、closureの参照カウント1
a = nil // Aクラスの参照カウント0

// クロージャ({ [weak self] in self?.count += 1 }が弱参照によってself(Aクラス)との循環参照が起こらない!

クロージャの中に[weak self]と記述することによって弱参照になり、Aクラスの参照がカウントされなくなりました。

これで、先ほどみたいにお互いが参照し合わないので循環参照が起こりません。

因みに[weak self]と書いている部分はキャプチャリストと呼ばれます。
こちらの記事を参考にしてみて下さい↓
【Swift】キャプチャ・リスト

おわりに

冒頭で述べた「[weak self]がないと循環参照してメモリーリークする」という意味が見えてきましたね。
実験的にやってみたい方はこの記事が参考になりますよ↓
SwiftやiOSの便利だけど忘れそうな小ネタ集

後で自分も実験してみて循環参照を肌で感じたいと思います。

参考資料

Swiftでなんで[weak self]するのか?
クロージャの中に書く[weak self] はじめからていねいに
【swift】弱参照と循環参照への対応について
【swift】ハマりがちな循環参照について

17
4
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
17
4