関数の引数の強参照で引き起こされるMemoryleakを防ぐ
先日メモリリークの調査をしたのですが、かなり見つけづらいパターンだなと思ったので、共有します。
実際に起きていた場所は、下記のようなUIView内でした。
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)
}
...
}
class MyViewController: UIViewController {
private var buttonView = ButtonView.instantiate()
...
override func viewDidLoad() {
super.viewDidLoad()
buttonView.putBottom(to: self)
}
...
}
一見ButtonView側でも [weak self]
していて安全な書き方に見えますが、
この≈はいつまでも破棄されず、メモリリークします。
原因:引数をクロージャ内でキャプチャしていることによる循環参照
循環参照によるメモリリークの例
よくある循環参照によるメモリリークの例として、
let classA = ClassA()
let classB = ClassB()
classA.child = classB
classB.child = classA
みたいなコードを見ることがあると思いますが、これは、互いに参照をもってしまっているために
ARCの参照カウンターがゼロにならずにメモリが解放されない為に起こります。
今回の例
今回の例はそれがさらに複雑になっています。
MyViewController -> ButtonView
の参照は明示的に書かれています。
では、ButtonView -> MyViewController
の参照はどうでしょう?
原因箇所はここです
...
//今回は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を引数に取る必要がありませんでした笑
なので、下記のような修正でメモリリークは解消されます。
...
//常に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はあまりいい設計とは言えません。
この場合、さらに関数内クロージャでの引数の参照を弱参照にすることでより安全になります。
...
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側でも対策することができます。
class MyViewController: UIViewController {
weak var buttonView: ButtonView?
...
override func viewDidLoad() {
super.viewDidLoad()
buttonView.putBottom(to: self)
}
...
}
このように、自身を参照する可能性のあるプロパティに関して、弱参照で定義することができます。
しかし、どこからも参照がなくなってしまったViewに関しては、すぐにdeallocateしてしまうのでこの対策には注意が必要です.
まとめ
今回のようなメモリリークを起こさないために、書き手としては、
- プロパティに自身を渡すような関数には気をつける
- 引数の強参照をふせぐため、保持されるクロージャ内では引数の弱参照を心がける
といった必要を感じました。実際起きてしまうとかなり気づきにくい上、ButtonViewの利用法等、時間が経って発覚する場合があるので、なるべく事前に防ぐ工夫が必要です。
Author And Source
この問題について(関数の引数の強参照で引き起こされるMemoryleakを防ぐ), 我々は、より多くの情報をここで見つけました https://qiita.com/Kazuma_Nagano/items/1666e605f266c4a95364著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .