withUnretained란
RxSwift 6부터 나온 기능으로, Observable 시퀀스에서 방출되는 요소들과 함께 특정 객체를 약한 참조로 묶어(unretained), 메모리 누수(Memory Leak)를 방지하는 데 사용합니다
* 이렇게 적으니 먼가 애매하죠? RxSwift공식 깃헙에 적힌 내용입니다만, 코드로 보는게 더 이해가 빠를 것 같네요
info
.subscribe(onNext: { [weak self] in
/// ~내용~~
})
.disposed(by: disposeBag)
클로저 내부의 self는 강한 참조로 캡처되며, disposeBag도 self에 의해 유지되므로 강한 참조 순환이 발생합니다. 이로 인해 ViewController가 더 이상 필요 없더라도 메모리에서 해제되지 않아 메모리 누수(Memory Leak)가 발생할 수 있습니다. 그렇기 때문에 저희는 다들 아시다시피 약한참조[weak self]로 메모리 누수를 강한 참조 순환을 방지합니다.
withUnretained은 이러한 [weak self]를 대채하여, 코드의 가독성을 높이고자 등장하였습니다.
withUnretained를 사용하면 코드가 더 간결하고 명확할 뿐만 아니라, 'guard let self = self else { return }'을 사용해서 self의 존재를 확인할 필요도 없습니다.
withUnretained 사용 예제
import RxSwift
class ViewController: UIViewController {
let disposeBag = DisposeBag()
let timerObservable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
override func viewDidLoad() {
super.viewDidLoad()
timerObservable
.withUnretained(self)
.subscribe(onNext: { (owner, value) in
owner.updateUI(value)
})
.disposed(by: disposeBag)
}
func updateUI(_ value: Int) {
print("Timer value: \(value)")
}
}
- 최상위 클로저에서 한 번 'withUnretaind(self)'를 사용해서 'self'를 안전하게 처리합니다.
- owner로 접근해서 코드를 간결하게 유지할 수 있습니다.
- 중첩 클로저 내부에서는 'weak owner'를 사용하여 약한 참조를 유지할 수 있습니다
self가 더 이상 존재하지 않을 때 구독을 자동으로 해제하여 메모리 관리가 더 안전합니다
불안정한 withUnretained
Driver에 대해 특별히 Unretained를 사용하여 더 이상 사용하지 마십시오.
withUnretained의 다른 구현에 대한 문서를 업데이트하여 재생 동작을 살펴보는 것이 명확해지도록 했습니다.
모든 항목(Observable, Driver, Signal, Infallible, Completable, Single, Maybe)에 대해 새로운 subscribe(with:onNext:etc:) 연산자를 추가합니다.
withUnretained은 사이드이펙트가 있습니다.
dirve에서 함께 사용할 경우, 작업 중인 스트림에 share(replay: 1) 작업이 있는 경우, replay buffer 는 값과 함께 self 를 버퍼링하므로 일부 시나리오에서 retain cycle 을 만들어낼 수 있습니다.
그 이유는 Driver 내부적으로 share(replay:1)을 사용하기 때문입니다.
Driver 는 특별하게 기본적으로 마지막 값을 반복하기 때문에 Driver 에 대해서 withUnretained 을 deprecated 해야 했습니다.
그렇게 등장한 subscribe(with:onNext:)
timerObservable
.subscribe(with: self, onNext: { owner, value in
owner.updateUI(value)
})
.disposed(by: disposeBag)
//MARK: Bind
textField.rx.text.orEmpty
.bind(with: self, onNext: { owner, text in
owner.label.text = text
})
.disposed(by: disposeBag)
//MARK: Driver
button.rx.tap
.asDriver()
.drive(with: self, onNext: { owner, _ in
owner.label.text = "Button Tapped"
})
.disposed(by: disposeBag)
- onNext 뿐만 아니라 onError, onCompleted, onDisposed 에도 약한 참조를 한 캡처리스트를 사용할 수 있습니다.(이전에는 onError 경우에는 withUnretained 를 사용하게 되도 약한 참조를 가지는 객체를 전달하지 못함)
결론
subscribe(with:onNext:)와 그와 유사한 bind, drive, emit의 확장 메서드를 사용하면 메모리 누수를 방지하면서도 코드의 가독성을 높일 수 있습니다. 특히, 클로저 내부에서 [weak self]와 옵셔널 바인딩을 반복하는 번거로움을 줄여줍니다.
'iOS > RxSwift' 카테고리의 다른 글
[RxCocoa] 델레게이트와 Rx의 중첩으로 인한 셀 클릭 인지 안되는현상 (0) | 2024.11.12 |
---|---|
[RxSwift] Single + Extension을 통해 로깅 시스템 구현 (0) | 2024.10.20 |
[RxSwift] PrimitiveSequenceType (0) | 2024.09.30 |
[RxSwift] Merge와 concat (0) | 2024.09.19 |
[RxSwift] 재사용으로 인해, 테이블뷰 셀안에 버튼이 반응하지 않을때 (0) | 2024.07.10 |