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되고disposedeinit 될 일이 없다면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()
}