RxSwift를 사용하다보면 순환참조로 인한 메모리 누수가 자주 일어나는데… 오늘 케이스 분석을 통해 같은 실수를 반복하지 말도록 해보아요…
RxSwift 순환참조 방지하기
RxSwift를 사용하면서 Memory Leak 이 빈번하게 발생된다. 코드를 작성할 때부터 신경써서 작성하지 않으면 순환참조 늪에 빠지게 된다.
그래서 오늘은 직접적으로 경험한 사례들을 기반으로 Memory Leak 을 해결하는 방법에 대해 공유하려고 한다.
순환참조 원인
순환참조는 기본적으로 상호간의 참조로 인하여 Memory Leak 의 직접적인 원인이다.
ViewController
-> disposeBag
-> subscription
-> self(ViewController)
위와 같은 형태로 보통 순환참조가 발생된다.
이 것을 해결하기 위하여 보통 [weak self] 또는 RxSwift에서 제공하는 withUnretained 메소드를 사용하기도 한다. 그리고 최근에는 subscribe(with:_) 를 통해 약한 참조를 걸 수 있도록 RxSwift에서 제공해주고 있다.
public func bind<Object: AnyObject>(
with object: Object,
onNext: @escaping (Object, Element) -> Void
) -> Disposable {
self.subscribe(onNext: { [weak object] in
guard let object = object else { return }
onNext(object, $0)
},
onError: { error in
rxFatalErrorInDebug("Binding error: \(error)")
})
}
RxSwift에서 bind를 할 때, 클로저내 self의 약한 참조를 지원하기 위하여 with 파라미터를 추가하였다. ( 이외의 메소드에서도 지원하고 있다. 그런데 왜 do() 메소드에는 지원하지 않고 있는걸까…? )
순환참조 코드 개선
Case 1
선언형 함수를 사용하자
// Before
self.viewModel.gpsButtonIsHidden
.drive(onNext: { self.gpsButton.rx.isHidden = $0 })
.disposed(by: self.disposeBag)
// After
self.viewModel.gpsButtonIsHidden
.drive(gpsButton.rx.isHidden)
.disposed(by: self.disposeBag)
bind를 하는 경우 항상 클로저 함수를 넣어야 하는 것은 아니다.
위와같이 바인딩 하는 과정에 클로저를 사용하지 않고 함수 선언으로 코드를 구현할 수 있다.
Case 2
// Before
rootView.contentView.menuErrorView.clickedRetryButton
.emit(with: self, onNext: { (owner, _) in
_ = owner._menuRepository.updateMenuItems()
.do(
with: self,
onSubscribe: { (owner) in
owner.rootView.contentView.menuErrorView.isLoading = true
},
onDispose: { (onwer) in
owner.rootView.contentView.menuErrorView.isLoading = false
}
)
.subscribe()
})
.disposed(by: disposeBag)
// closure 안에서 self 를 참조해버림..... "with: self"
// After
rootView.contentView.menuErrorView.clickedRetryButton
.emit(with: self, onNext: { (owner, _) in
_ = owner._menuRepository.updateMenuItems()
.do(
onSubscribe: { owner.rootView.contentView.menuErrorView.isLoading = true },
onDispose: { owner.rootView.contentView.menuErrorView.isLoading = false }
)
})
.disposed(by: disposeBag)
( 일단 emit 클로저 안에 새로운 subscribe을 수행하는 것이 조금 불편하지만 당장의 문제를 해결하는 것에 집중해보자… )
Before 코드를 보면 emit(with:) 메소드를 통해 약한 참조 될 수 있도록 하였다. 하지만 문제는 closure 구문안에 새로운 subscribe 가 있고 do(with:) 메소드를 또 사용하고 있다.
그런데 여기서 do(with: self) 메소드가 문제가 된다. 약한 참조를 만들기 위한 코드이지만 emit() closure 안에서 self를 참조하고 말았다.
이렇게 의식없이 코드를 짜면 매일 문제가 생긴다.. 생각은 길게 코드 작성은 짧게….
Case 3
// Before
self.action.checkBeaconRecetionStatus
.do(onNext: { [weak self] in
self?.processBeaconRecetionStatus()
})
.subscribe()
.disposed(by: disposeBag)
// After
self.action.checkBeaconRecetionStatus
.subscribe(with: self, onNext: { (owner, _) in
owner.processBeaconRecetionStatus()
})
.disposed(by: disposeBag)
해당 코드에서는 순환참조가 발생되진 않았지만, do() 메소드 대신 subscribe 메소드를 사용하자는 취지에서 참조하였다.
Case 4
// Before
let openAction = UIAlertAction(
title: title,
style: .default,
handler: { _ in
action()
self.showNextWaitingAlert()
}
)
// After
let openAction = UIAlertAction(
title: title,
style: .default,
handler: { [weak self] _ in
action()
self?.showNextWaitingAlert()
}
)
해당 코드는 자주 실수하는 부분이다.
open class UIAlertAction : NSObject, NSCopying {
public convenience init(title: String?, style: UIAlertAction.Style, handler: ((UIAlertAction) -> Void)? = nil)
}
Action을 생성하려고 할 때 handler에 @escaping
키워드가 포함되어 있지 않아 [weak self] 를 사용하지 않는 경우가 있다.
하지만 AlertAction handler를 보면 ((UIAlertAction) -> Void)?
타입으로 선언되어 있다. 이 경우 @escaping
를 명시할 수 없어서 작성되지 않은 것이지 Optional의 value 값에 탈출 클로저가 담길 수 있는 것이다.
그러므로 Optional closure인 경우 항상 [weak self] 약한 참조 처리를 해야 한다!
@escaping
클로저는 순환참조를 발생시킬 수 있다.non-escaping
클로저는 순환참조를 발생시키지 않는다.
Case 5
// Before
self.context.items
.map { (items) in
items.map { [weak self] (item) in
TabBarItemButton(item, badge: self?.context.badgeStore.findByAppName(appName: item.menuItem.appName)?.driver)
}
}
.driver
.drive(onNext: { [weak self] (buttons) in
self?.itemButtons.accept(buttons)
})
.disposed(by: self.disposeBag)
// After
self.context.items
.map { [weak self] (items) in
items.map { (item) in
TabBarItemButton(item, badge: self?.context.badgeStore.findByAppName(appName: item.menuItem.appName)?.driver)
}
}
.driver
.drive(onNext: { [weak self] (buttons) in
self?.itemButtons.accept(buttons)
})
.disposed(by: self.disposeBag)
해당 코드의 경우는 closure의 범위에 따른 [weak self] 처리를 잘못한 경우이다.
Before 코드에서는 closure { closure { [weak self] } }
클로저 내부에 약한 참조를 하고 있기 때문에 결과적으로 순환참조가 발생되었다.
위와 같은 케이스에서는 closure { [weak self] closure { } }
와 같이 외부 클로저에 약한 참조 처리를 해야 한다.