Swift - Memory Leak: Struct와 Class

3 분 소요


이 때까지 좀 잔바리들만 한 거 같아서 슬슬 머리 아픈 거 좀 보겟슴다


이런 ViewModel의 문제??

지금 하던 프로젝트 거 코드를 조금 단순화해서 들고 왔습니다

import RxSwift
import RxCocoa

struct SomeVM {
    var disposeBag = DisposeBag()
    let someRepository = SomeRepository()
    
    let someRelay = PublishRelay<Bool>()
    
    struct Input {
        let some: Observable<Bool>
    }
    
    struct Output {
        let someResult: Observable<Bool>
    }
    
    func transform(input: Input) -> Output {
        input.some
            .flatMap { isScrap in
                self.someRepository.foo()
            }
            .asObservable()
            .subscribe(onNext: { result in
                self.someRelay.accept(result)
            })
            .disposed(by: disposeBag)

        return Output(
            someResult: someRelay.asObservable()
        )
    }
}

위 코드처럼 간단한 VM이 있습니다

원하는 동작 로직은 아래와 같다고 봅시다.

  • 인풋으로 some이라는 옵저버블을 받고
  • some에게서 이벤트를 수신하면 someRepository을 통해 네트워크 작업을 하자
  • 그럼 그 결과를 subscribe해서 someRelay에 중계해줘야지
  • VM의 아웃풋은 someRelay를 옵저버블로 해주면 VC에서 알아서 잘 쓰겠지??
ViewController Input/Output ViewModel
some 입력 someRepository.foo()
   
무언가 아웃풋 someRelay 출력

여기서 SomeRepositoryMoya를 사용한 네트워크 통신 class임니다
문제점을 아시겠나요

메모리가 증식해

커맨드 i를 눌러서 Memory leak을 확인해 보아요

1

헐… 이게 모임
그냥 저 화면 쓰기만 하면 메모리 릭이 잔뜩 생기네

이거 왜 이런 거임?? 어케 해결 함

대답 1

struct SomeVM {
    var disposeBag = DisposeBag()
    let someRepository = SomeRepository()
    
    let someRelay = PublishRelay<Bool>()
    
    struct Input {
        let some: Observable<Bool>
    }
    
    struct Output {
        let someResult: Observable<Bool>
    }
    
    func transform(input: Input) -> Output {
        input.some      // Add!!
            .flatMap { [unowned self] isScrap in
                self.someRepository.foo()
            }
            .asObservable()     // Add!!
            .subscribe(onNext: { [unowned self] result in
                self.someRelay.accept(result)
            })
            .disposed(by: disposeBag)

        return Output(
            someResult: someRelay.asObservable()
        )
    }
}

어휴~ 클로저에서 self.someRepositoryself.someRelay 쓰는데~~ self 저렇게 쓰면 강한 참조네요!! 캡쳐 리스트는 적어 줘야지~~

하면 반쯤 정답

아니면 오히려 마이너스…
어디서 들은 거만 있던 저는 저렇게 생각하고 붙여줬다가 에러 떠서 당황햇네여

SomeVMstruct이기 때문에 그런 걸 적을 수 없슴니다(참조가 아니니까 ARC는 struct한텐 그런 거 안 해요).
공부 똑바로 해야지

어쨌든 일단 반쯤 정답이라고 한 이유는…

대답 2

// Change!!
class SomeVM {
    var disposeBag = DisposeBag()
    let someRepository = SomeRepository()
    
    let someRelay = PublishRelay<Bool>()
    
    struct Input {
        let some: Observable<Bool>
    }
    
    struct Output {
        let someResult: Observable<Bool>
    }
    
    func transform(input: Input) -> Output {
        input.some      // Add!!
            .flatMap { [unowned self] isScrap in
                self.someRepository.foo()
            }
            .asObservable()     // Add!!
            .subscribe(onNext: { [unowned self] result in
                self.someRelay.accept(result)
            })
            .disposed(by: disposeBag)

        return Output(
            someResult: someRelay.asObservable()
        )
    }
}

거기서 struct이던 SomeVMclass로 바꿔주기만 하면 해결될 거 같은데요

structclass로 바꿔주고 캡쳐 리스트를 적어주면 메모리 증식 문제가 해결 됩니다

1

편안하네요

결론: struct가 property로 class를 갖고 있다면, 해당 프로퍼티가 혹시 외부로 캡쳐 되는 지 잘 확인해 주자.

간단한 예시

코드를 좀 더 단순화 해서 적어 보았습니다

1. struct가 class 프로퍼티를 가지는데 걔가 캡쳐 될 때
struct S {
    var c = C()

    func closure() -> (() -> ()) {
        return {
            c.printC()
        }
    }
}
class C {
    func printC() {
        print("C야")
    }

    deinit {
        print("C가 deinit")
    }
}
// 실행
var s: S? = S()

let closure = s!.closure()
closure()
closure()

s = nil
closure()

이런 코드를 한 번 봅시다. 실행 결과는 아래와 같습니다

C야
C야
C야

헉 왜 deinit 안 됨?? 소름돋네요… s가 nil이 됐는데도 c가 계속 출력을 하네?? 정말 신기한 일입니다

참조 상태

=>: 강한 참조
s => c
closure => c

이유는?? s가 일단 c를 참조하고 있는 건 당연한데, closurec를 참조하고 있기 때문
s가 nil이 된다고 해도 여전히 살아 있습니다.

2. 그럼 어케 해제할 수 있을까
struct S {
    var c = C()

    func closure() -> (() -> ()) {
        return {
            c.printC()
        }
    }
}
class C {
    func printC() {
        print("C야")
    }

    deinit {
        print("C가 deinit")
    }
}

이까진 같고

// 실행
var s: S? = S()

var closure: (() -> ())? = s!.closure()
(closure ?? {})()
(closure ?? {})()

s = nil
(closure ?? {})()

closure = nil
(closure ?? {})()

즉 이렇게 closure까지 nil이 되어야 c가 진짜로 deinit 가능합니다.
실행 결과는 아래와 같습니다.

C야
C야
C야
C가 deinit
3. class가 class 프로퍼티를 가지는데 걔가 캡쳐 될 때
class S {
    var c = C()

    func closure() -> (() -> ()) {
        return { [weak self] in
            self?.c.printC()
        }
    }
    
    deinit {
        print("S가 deinit")
    }
}

Sclass로 바꾸고

class C {
    func printC() {
        print("C야")
    }

    deinit {
        print("C가 deinit")
    }
}

얘는 같고

// 실행
var s: S? = S()

let closure = s!.closure()
closure()
closure()

s = nil
closure()

이제 실행 결과는 아래와 같습니다

C야
C야
S가 deinit
C가 deinit

참조 상태

=>: 강한 참조, ->: 약한 참조
s => c
closure -> s

참조 상태는 이렇게 되니까, s가 없어지면 해당 클로저 안의 구문은 실행이 안 되겠네요



“이야~ 서이프트가 서터럭터 쓰면 좋다매!!” 해서 무지성 스트럭트 썼더니 이런 일이 생기네요
이래서 어디서 대충 주서 들은 걸로 하면 안 되는가 봅니다

네트워크 통신 등을 하는 뷰모델은 단순히 데이터를 저장하는 역할도 아니기 때문에 스트럭트일 필요가 없는 것 같다.


태그: ,

카테고리:

업데이트:

댓글남기기