関数の引数の強参照で引き起こされるMemoryleakを防ぐ

先日メモリリークの調査をしたのですが、かなり見つけづらいパターンだなと思ったので、共有します。

実際に起きていた場所は、下記のようなUIView内でした。

Swiftにおけるメモリリーク、参照カウント、強参照については別記事を参考にしていただけると幸いです。

##実際の発生コード
クラス名等は変更しています。

```ButtonView.swift
...

//常にViewControllerの下部に浮かせて表示したいボタン
class ButtonView: UIView {
    private let disposeBag = DisposeBag()
    ...

    func putBottom(to viewController: UIViewController) {
        viewController.view.addSubview(self)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor).isActive = true
        self.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor).isActive = true
        self.heightAnchor.constraint(equalToConstant: 70).isActive = true
        
        //今回はRxSwiftを利用した例ですが、破棄されないクロージャであればなんでも良いです。
        viewController.view.rx.methodInvoked(#selector(viewController.view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }

    ...

}
```

```MyViewController.swift
class MyViewController: UIViewController {
    private var buttonView = ButtonView.instantiate()
    
    ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        buttonView.putBottom(to: self)
    }

    ...
}
```

一見ButtonView側でも `[weak self]` していて安全な書き方に見えますが、
この≈はいつまでも破棄されず、メモリリークします。

## 原因：引数をクロージャ内でキャプチャしていることによる循環参照

### 循環参照によるメモリリークの例
よくある循環参照によるメモリリークの例として、

```.swift
let classA = ClassA()
let classB = ClassB()

classA.child = classB
classB.child = classA
```

みたいなコードを見ることがあると思いますが、これは、互いに参照をもってしまっているために
ARCの参照カウンターがゼロにならずにメモリが解放されない為に起こります。

### 今回の例
今回の例はそれがさらに複雑になっています。
`MyViewController -> ButtonView` の参照は明示的に書かれています。
では、`ButtonView -> MyViewController` の参照はどうでしょう？

原因箇所はここです

```ButtonView.swift
   ...
   //今回はRxSwiftを利用した例ですが、破棄されないクロージャであればなんでも良いです。
   viewController.view.rx.methodInvoked(#selector(viewController.view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                //クロージャ内で関数の引数であるviewControllerをキャプチャしている
                viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }
    ...

}
```

このクロージャはdisposeBagが破棄されたタイミング、つまりButtonViewの破棄されるタイミングで参照がリセットされますが、今回の例ではクロージャ内で引数であるviewControllerがクロージャ内でキャプチャされています。
これにより、暗黙的に `ButtonView -> MyViewController` の参照が生まれていて、循環参照となっていました。


## 修正と対策

### viewControllerを引数に渡す関数設計をやめる
今回の例では、実はそもそもviewControllerを引数に取る必要がありませんでした笑
なので、下記のような修正でメモリリークは解消されます。

```ButtonView.swift
...

//常にViewControllerの下部に浮かせて表示したいボタン
class ButtonView: UIView {
    private let disposeBag = DisposeBag()
    ...

    func putBottom(to view: UIView) {
        view.addSubview(self)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.leadingAnchor.constraint(equalTo:view.leadingAnchor).isActive = true
        self.trailingAnchor.constraint(equalTo:view.trailingAnchor).isActive = true
        self.heightAnchor.constraint(equalToConstant: 70).isActive = true
        
        //今回はRxSwiftを利用した例ですが、破棄されないクロージャであればなんでも良いです。
        view.rx.methodInvoked(#selector(view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }

    ...

}
```

`MyViewController.view-> ButtonView` への参照はないため、どちらもMyViewControllerのdeallocateのタイミングでメモリが解放されます。

### 関数内クロージャでの引数の参照を弱参照にする
ですが、`UIView`利用の仕方によってメモリリークするButtonViewはあまりいい設計とは言えません。
この場合、さらに関数内クロージャでの引数の参照を弱参照にすることでより安全になります。

```ButtonView.swift
   ...
   viewController.view.rx.methodInvoked(#selector(viewController.view.safeAreaInsetsDidChange))
            .take(1)
            .subscribe(onNext: { [weak self, weak view] _ in
                guard let self = self, let view = view else { return }
                //クロージャ内で関数の引数であるviewControllerをキャプチャしている
                viewController.view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive  = true
            })
            .disposed(by: disposeBag)
    }
    ...

}
```
あまり見慣れない書き方ですが、クロージャ内で複数の引数を弱参照でキャプチャしたい場合は `[weak self, weak view]` とかけます。
この場合、仮に `引数のView -> ButtonView`の参照があった場合でも、メモリリークを起こすことなく利用できます

### 利用するViewController側での対策
利用するViewController側でも対策することができます。

```MyViewController.swift
class MyViewController: UIViewController {
    weak var buttonView: ButtonView?
    
    ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        buttonView.putBottom(to: self)
    }

    ...
}
```
このように、自身を参照する可能性のあるプロパティに関して、弱参照で定義することができます。
しかし、どこからも参照がなくなってしまったViewに関しては、すぐにdeallocateしてしまうのでこの対策には注意が必要です.

## まとめ
今回のようなメモリリークを起こさないために、書き手としては、
`- プロパティに自身を渡すような関数には気をつける`
`- 引数の強参照をふせぐため、保持されるクロージャ内では引数の弱参照を心がける`

といった必要を感じました。実際起きてしまうとかなり気づきにくい上、ButtonViewの利用法等、時間が経って発覚する場合があるので、なるべく事前に防ぐ工夫が必要です。