Borysarang·2022년 10월 4일
0

swift

목록 보기
7/7

RxCocoa - Button Tap gesture binding MemoryLeak 문제 - [weak self]쓰는 이유에 대하여

tags: 문제 해결

리뷰어인 제임스와의 리뷰를 통해 생각해본 내용을 정리해보았다.

상황

다음과 같이 할일을 추가하기위해 present된 뷰의 RightBarButton에 RxCocoa를 사용하여 버튼 탭 이벤트를 연결시켜 주었다.

func setupRightBarButtonItem() -> UIBarButtonItem {
    let rightBarButtonItem = UIBarButtonItem(systemItem: .done)
    rightBarButtonItem.rx.tap.bind {
        self.doneButtonDidTapped()
    }.disposed(by: disposedBag)
        
	return rightBarButtonItem
}
func doneButtonDidTapped() {
    if isNewTask != nil {
        guard let extractedTask = extractCurrentTask() else {
            return
        }    
        viewModel?.createTask(to: extractedTask, at: .TODO)
    } else {
        editButtonDidTapped()
    }
    self.dismiss(animated: true)
}

우측 Done버튼을 눌렀을 경우의 이벤트 stream을 바인딩하여 dismiss되도록 하였다.

문제

다만 다음과 같이 셀을 반복해서 클릭할 시

아래처럼 메모리가 해제가 안되는 현상을 발견할 수 있었다.

이 문제의 원인으로 다음과 같이 추측해보았다.

  • TodoAddView가 dismiss될 때 ViewModel이 해제되지 않는다.
    • 이 부분은 확인해 보았으나 강한참조로 엮이는 부분이 없었다.
  • MainViewController에서 인스턴스화 한 DisPosableBag에 쌓이기만 하고 할당해제가 안되서 계속해서 rx관련 데이터가 남아있는다
    • 다만 viewDidDisAppear에서 확인 시 해제가 안되는것으로 보아 TodoAddView의 문제인것으로 생각되었다.
  • TodoAddViewController가 dismiss되면서 어떤 객체가 메모리에서 해제 되지 않는다.
    • deinit시에 로그를 찍어보니 TodoAddViewController가 deinit되지 않고 있음을

하지만 ViewModel의 문제가 아님을 확인했으므로 Rx관련 코드에서 어떤 문제가 일어나는것 같았으나 정확히 문제를 확인할 수 없었다.

해결

위의 코드에서 UIBarButtonItem을 래핑한 Rx코드는 다음과 같다

extension Reactive where Base: UIBarButtonItem {
/// Reactive wrapper for target action pattern on `self`.
    public var tap: ControlEvent<()> {
        let source = lazyInstanceObservable(&rx_tap_key) { () -> Observable<()> in
            Observable.create { [weak control = self.base] observer in
                guard let control = control else {
                    observer.on(.completed)
                    return Disposables.create()
                }
                let target = BarButtonItemTarget(barButtonItem: control) {
                    observer.on(.next(()))
                }
                return target
            }
            .take(until: self.deallocated)
            .share()
        }
        
        return ControlEvent(events: source)
    }
}

guard 부분을 보면 tap gesture는 create으로 observable을 생성하고 있다.

Observable을 생성하는 방법중에 create과 just 등이 있지만 just가 좀 더 넓은 개념을 포함하고 있다.

final private class Just<Element>: Producer<Element> {
    private let element: Element
    
    init(element: Element) {
        self.element = element
    }
    
    override func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable where Observer.Element == Element {
        observer.on(.next(self.element))
        observer.on(.completed)
        return Disposables.create()
    }
}

create은 단지 Observable을 생성하지만, just는 create + onNext + onComplete(또는 onError) + Disposable.create을 포함한다. 반면에 create은 Disposable 타입을 스트림에 넘겨주지 않고있다.

코드를 보면 tap 제스쳐 자체가 onComplete로 이벤트 스트림을 종료시키지 않고 있고, 여기에 바인딩된 클로저가 self.doneButtonDidTapped()와 같이 TodoAddViewController를 참조하고 있었다.

결과적으로 tap 제스쳐 자체에서 onComplete되지 않으므로 bind의 클로저가 강한참조로 엮여있어 TodoAddViewController가 deinit되지 않은것이었다.

왜 Tap 제스쳐에서는 onComplete등을 통해서 스트림을 완료하지 않을까?

버튼은 대게 일회성이 아닌 여러번 눌림을 상정하고 사용된다.

let target = BarButtonItemTarget(barButtonItem: control) {
    observer.on(.next(()))
}

tap 제스쳐 내부의 타겟을 정해주는 부분에서 이부분에서 complete를 시전한다면 어떻게 될까?
아마 1회용 버튼액션이 등록될 것같다. 이벤트가 들어오는걸 rx가 메모리에 올라간 상태로 대기하고 있어야하는데 첫번째 눌리면서 onComplete되어 더이상 이를 수행하지 않기 때문이다.

결국 Tap 제스쳐를 사용하여 bind할 경우 왠만하면 [weak self]를 사용해야 하는것이다.

사실 [weak self]를 rx 사용하는 부분에서 대부분 사용하면 내가 했던 실수는 해결할 수 있다고 한다. 하지만 이런, "왜 쓰는지를 아는 것" 즉 알고 쓰는게 중요하다고 생각했다.

0개의 댓글