Swift - Memory Leak: Struct와 Class
이 때까지 좀 잔바리들만 한 거 같아서 슬슬 머리 아픈 거 좀 보겟슴다
이런 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 출력 |
여기서 SomeRepository
는 Moya
를 사용한 네트워크 통신 class
임니다
문제점을 아시겠나요
메모리가 증식해
커맨드 i를 눌러서 Memory leak을 확인해 보아요
헐… 이게 모임
그냥 저 화면 쓰기만 하면 메모리 릭이 잔뜩 생기네
이거 왜 이런 거임?? 어케 해결 함
대답 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.someRepository
랑self.someRelay
쓰는데~~ self 저렇게 쓰면 강한 참조네요!! 캡쳐 리스트는 적어 줘야지~~
하면 반쯤 정답
아니면 오히려 마이너스…
어디서 들은 거만 있던 저는 저렇게 생각하고 붙여줬다가 에러 떠서 당황햇네여
SomeVM
은 struct
이기 때문에 그런 걸 적을 수 없슴니다(참조가 아니니까 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
이던SomeVM
을class
로 바꿔주기만 하면 해결될 거 같은데요
넵 struct
를 class
로 바꿔주고 캡쳐 리스트를 적어주면 메모리 증식 문제가 해결 됩니다
편안하네요
결론: 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
를 참조하고 있는 건 당연한데, closure
도 c
를 참조하고 있기 때문
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")
}
}
S
를 class
로 바꾸고
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가 없어지면 해당 클로저 안의 구문은 실행이 안 되겠네요
“이야~ 서이프트가 서터럭터 쓰면 좋다매!!” 해서 무지성 스트럭트 썼더니 이런 일이 생기네요
이래서 어디서 대충 주서 들은 걸로 하면 안 되는가 봅니다
네트워크 통신 등을 하는 뷰모델은 단순히 데이터를 저장하는 역할도 아니기 때문에 스트럭트일 필요가 없는 것 같다.
댓글남기기