SwiftUI에서 메모리가 해제되지 않고 사용량이 과도하게 늘어난다는 생각을 하게 되었다
메모리 누수라고 오해했던 부분과 ObservableObject에 대한 이야기를 해보려고 한다
Xcode의 Debug navigation을 확인해 보면 앱에서 새로운 데이터를 가져오고 화면에 보여줄 때마다 메모리 사용량이 증가한다는 것을 알 수 있다. 하지만 뷰가 사용되지 않을 때, 다른 화면으로 넘어갈 때 가지고 있던 뷰 모델이 사용한 메모리를 자동으로 반납을 하지 않는 것을 발견했다
이 문제를 처음에는 메모리 이슈라고 생각해서 Instrument로 확인해 보았지만 Retain Count를 포함한 모든 것이 정상이었다. 그리고 의심을 하게 된 것이 ObservableObject였다. @StateObject는 뷰의 라이프 사이클에 의존하지 않기 때문에 화면이 내려가더라도 API로부터 받은 데이터들이 뷰 모델이 남아있을 거라고 생각했고 그렇다면 @StateObject를 사용하지 않거나 최상위의 객체로부터 @ObservedObject로 넘겨받아야 하는가라고 생각했지만 어디에서든 @StateObject가 사용된다면 메모리는 쌓이기만 할 것이었다
여기서 한 가지 생각을 하게 되었는데 API로부터 받은 1MB도 넘지 않는 데이터가 메모리에 영향을 줄 일이 없다는 것이었다. 그렇다면 문제가 될 요소는 Cache된 이미지들 밖에 없었다.
이미지의 Cache 처리를 위해 Kingfisher 라이브러리를 사용하고 있는데 구글링 결과 다음과 같은 답을 얻었다
The images are cached to memory as well by default. If you did not change anything, it takes **at most 25% memory of your device**. When you click the back button, the memory would still be in memory if the purge condition is not met.
https://github.com/onevcat/Kingfisher/issues/1268
기본적으로 특정 조건이 발생하기 전까지는 최대 기기의 메모리 크기 전체의 25%를 사용할 수 있다는 설명이다
private static func createMemoryStorage() -> MemoryStorage.Backend<KFCrossPlatformImage> {
let totalMemory = ProcessInfo.processInfo.physicalMemory
let costLimit = totalMemory / 4
let memoryStorage = MemoryStorage.Backend<KFCrossPlatformImage>(config:
.init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit)))
return memoryStorage
}
아직 특정 조건에 대해서 다 알지는 못하지만 앱이 백그라운드로 내려가거나 하는 상황에서는 메모리를 돌려주었다
그리고 다음 코드로 수동으로 캐시 데이터를 비워줄 수 있다는 것도 우연히 알게 되었다
.onDisappear {
let cache = ImageCache.default
cache.clearMemoryCache()
cache.clearDiskCache()
}
결론적으로 메모리의 누수도 ObservableObject의 의도하지 않은 동작도 아니었다. 단지 라이브러리와 데이터 캐싱에 대한 이해가 없어서 발생한 해프닝이었다
아직 전체 메모리 크기의 1/4을 사용하는 테스트를 해보지 않았지만 자동으로 캐시 데이터가 삭제되는 조건과 시점을 안다면 조금 더 안심하고 개발할 수 있을 것 같다.