Subscribe 중인 Stream을 메모리에서 해제(리소스 정리)하기 위해서는 해당 Stream을 dispose하는 과정이 필요하다.
어느 상황에서 dispose가 실행될 수 있는지 정리해 보았다
일반적으로 사용하는 subscribe
함수는 반환값이 존재한다
let textArray = BehaviorSubject(value: ["Hue", "Jack", "Koko", "Bran"])
// 구독하기 -> 반환값의 타입은? : Disposable
textArray.subscribe(with: self) { owner , value in
print("onNext - \(value)")
} onError: { owner , error in
print("onError - \(error)")
} onCompleted: { owner in
print("onCompleted")
} onDisposed: { owner in
print("onDisposed")
}
Disposable
이라는 프로토콜인 걸 확인할 수 있다어쨌든 이 반환값을 사용해야 하고, 그래서 일반적으로 disposeBag
에 담는 코드를 뒤에 붙여준다
let textArray = BehaviorSubject(value: ["Hue", "Jack", "Koko", "Bran"])
// 구독하기 -> 반환값의 타입은? : Disposable
textArray.subscribe(with: self) { owner , value in
print("onNext - \(value)")
} onError: { owner , error in
print("onError - \(error)")
} onCompleted: { owner in
print("onCompleted")
} onDisposed: { owner in
print("onDisposed")
}
.disposed(by: disposeBag)
그럼 dispose되는 시점에 어느 차이가 있는지 확인해보자
let textArray = BehaviorSubject(value: ["Hue", "Jack", "Koko", "Bran"])
let textArray = Observable.from(["Hue", "Jack", "Koko", "Bran"])
/* subscribe 코드는 위와 동일하다 */
Observable
로 선언한 경우, onNext
로 방출이 끝나면 그 즉시 onCompleted
가 실행되고, onDisposed
까지 실행되면서 dispose
가 완료되었음을 확인할 수 있다Subject
로 선언한 경우, onNext
실행 이후 별다른 코드가 보이지 않는다Observable
은 데이터를 방출한 순간, 역할을 다 했다. 하지만 Subject
는 Observer
의 역할, 즉 데이터를 전달받을 수도 있기 때문에 아직 역할이 남았다고 표현할 수 있다.Subject
는 onCompleted
가 실행되지 않고, dispose
도 역시 실행되지 않는다dispose
는 리소스가 정리됨을 의미하고, 메모리가 해제됨을 의미한다.dispose
가 실행된다onError
또는onCompleted
가 실행된다면 그 즉시 dispose
가 실행될 것이다onCompleted
에 대해서는 2번에서 확인했으므로, onError
를 실행시켜보자onError
실행시키는 것도 생각보다 간단하진 않다
enum JackError: Error { // Error 프로토콜의 열거형
case invalid
}
textArray.onNext(["hihi"])
textArray.onNext(["ho"])
textArray.onError(JackError.invalid) // Error 이벤트 전달
textArray.onNext(["a", "b", "c"]) // 여긴 전달이 되지 않을 것이다
textArray.onNext(["d", "e", "f"])
onError
이벤트가 실행된 순간, dispose
가 실행되는 것을 확인할 수 있다
당연히 dispose
된 이후에 onNext
로 전달한 이벤트는 받을 수 없다
subscribe
이후에는 disposeBag
이라는 곳에 Stream을 담아둔다onError
또는 onCompleted
가 실행되면 알아서 dispose
가 실행된다onError
나 onCompleted
가 실행되지 않았더라도 내 맘대로 dispose
시키고 싶을 수도 있다. 즉, 내가 원하는 시점에 dispose
를 실행시키고 싶다subscribe
한 Stream 자체를 변수에 담는다let textArrayValue = textArray
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
Disposable
이다Disposable
프로토콜의 정의를 타고 들어가면 dispose
메서드가 있고, 이게 내가 해야 실행시켜야 하는 메서드이다.DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
textArrayValue.dispose() // "dispose" : 직접적으로 리소스를 정리함
}
dispose
하기 위해서onError
또는 onCompleted
가 실행되거나.disposed(by: disposeBag)
은 뭐냐disposBag
을 통해 dispose
메서드가 실행되고 있었다DisposeBag
클래스의 정의를 타고 들어가보자
(설명에 필요하지 않은 내용은 지웠다)
public final class DisposeBag: DisposeBase {
// state
private var disposables = [Disposable]()
/// This is internal on purpose, take a look at `CompositeDisposable` instead.
private func dispose() {
let oldDisposables = self._dispose()
for disposable in oldDisposables {
disposable.dispose()
}
}
private func _dispose() -> [Disposable] {
self.lock.performLocked {
let disposables = self.disposables
self.disposables.removeAll(keepingCapacity: false)
self.isDisposed = true
return disposables
}
deinit {
self.dispose()
}
}
disposeBag
에 담아둔 Stream들이 disposables
라는 배열에 저장된다dispose
메서드가 실행되면, 그 배열을 반복문으로 돌면서 각 Stream에 대해 dispose()
를 실행한다. 위에서 내가 직접 dispose()
를 실행시킨 부분과 동일하다dispose
가 다르다는 점을 주의하자dispose
메서드가 실행되는 시점은, 인스턴스가 deinit
될 때이다let disposeBag = DisposeBag()
인스턴스가 deinit
될 때,disposeBag
이 물고 있던(?) 모든 Stream에 대해 dispose
가 실행된다.deinit
되는 순간 메모리 정리가 싹 되기 때문에 메모리 누수가 발생하지 않는다deinit
되면, Stream들의 메모리 누수 걱정을 할 필요가 없다deinit
이 실행되지 않을 것이다. 만약 rootVC의 Stream에 대해 dispose
를 실행해야 한다면 어떻게 해야 할까첫 번째 방법은 4번에서 했던 것처럼 모든 Stream에 대해 직접 dispose
메서드를 실행하는 것이다
4번과 동일하게, disposeBag
에 담지 않고 모든 Stream을 상수에 담아주었다
let increment = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
let incrementValue = increment
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
let incrementValue2 = increment
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
let incrementValue3 = increment
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
// 필요한 시점에, 내가 직접 dispose 한다!
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
incrementValue.dispose()
incrementValue2.dispose()
incrementValue3.dispose()
}
disposeBag
이 생각나게 된다. disposeBag
이 해줬던 것처럼, 물고 있는 모든 Stream에 대해 한 번에 dispose
를 실행시킬 수는 없을까?disposeBag
의 기능을 사용하기 위해, 실행되어야 하는 것은 disposeBag 인스턴스의 deinit 메서드이다.deinit
될 때, 당연히 disposeBag
도 deinit
된다고 소개했다.disposeBag
의 deinit
을 실행시킬 수 있다인스턴스를 교체한다
let increment = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
increment
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
.disposed(by: disposeBag)
increment
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
.disposed(by: disposeBag)
increment
.subscribe(with: self) { owner , value in
print("next - \(value)")
} onError: { owner , error in
print("error - \(error)")
} onCompleted: { owner in
print("completed")
} onDisposed: { owner in
print("disposed")
}
.disposed(by: disposeBag)
// 필요한 시점에, 내가 직접 dispose 한다!
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.disposeBag = DisposeBag()
}
dispose
: 메모리에서 해제. 리소스 정리onError
, onCompleted
실행Disposable
프로토콜의 dispose
메서드 직접 실행DisposeBag
클래스 이용deinit
되면disposeBag
인스턴스도 deinit
되고dispose
deinit
될 일이 없다면disposeBag
인스턴스 교체disposeBag
의 deinit
실행셀 위의 버튼.rx.tap
과 화면 전환 코드(navigation push
)를subscribe
로 연결한다. (VC의 cellForRowAt 부분에서 작성한다)/* SearchTableViewCell 의 인스턴스 */
let appNameLabel: UILabel
let appIconImageView: UIImageView
let downloadButton: UIButton // 화면 전환을 연결할 버튼
var disposeBag: DisposeBag
/* SearchTableViewController */
var data = ["a", "b", "ab", "abcde", "de", "db", "abcd"] // 데이터 변경 시 편의를 위해 따로 배열을 관리한다
lazy var items = BehaviorSubject(value: data)
let disposeBag = DisposeBag()
func bind() {
// cellForRowAt
items.bind(to: tableView.rx.items(
cellIdentifier: SearchTableViewCell.identifier,
cellType: SearchTableViewCell.self
)) { (row, element, cell) in
cell.appNameLabel.text = element
cell.appIconImageView.backgroundColor = .green
// 버튼과 화면 전환 구독
cell.downloadButton.rx.tap
.subscribe(with: self) { owner , value in
owner.navigationController?.pushViewController(SampleViewController(), animated: true)
}
.disposed(by: cell.disposeBag) // cell 인스턴스의 disposeBag을 사용한다
}
.disposed(by: disposeBag)
}
subscribe
를 해주고 있는 꼴이 된다subscribe
로 연결한 액션은 화면 전환이기 때문에, 버튼을 한 번 누르면 연속해서 화면 전환이 일어난다prepareForReuse
에서 해결할 수 있는 것 같다prepareForReuse
에 코드를 작성해주면 되는데,disposeBag을 교체해준다
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}