본문 바로가기

iOS/SwiftUI

SwiftUI) NavigationLink와 Memory Leak

Memory Leak

지난 글인 ViewBuilder 와 guard let에서 보면 메모리 누수가 발생한 것을 알 수 있다. 이를 고치려고 AnyView를 대체했지만 소용이없었다. 그러던 중 이미지를 네트워크 api 통신을 통해 불러오는 ImageLoader객체가 비이상적으로 생성된 것을 발견하였다.

스크린샷 2020-12-04 오후 10 40 57

현재 네이버 바이브 클론 프로젝트를 진행중인데 음악 스트리밍 서비스인 만큼 이미지가 많이 필요한 상황이라 이미지 객체를 포기할 수 없어서 고쳐보기로 했다. 사실 뷰는 중요하지 않고 다른 것이 중요한 상황이라 이미지는 정적 이미지로 고정할 수도 있었지만 여태 만든 DB 더미 데이터와 네트워크 객체를 포기하기 아까워 고치기로 했다.

loginPageloginPage

홈 화면(Today Scene)과 더보기 화면(Magazine Scene)을 왔다갔다 이동하며 확인해본 결과 이미지 객체가 누적되어 생성되지는 않았다. 그냥 하나 생길 것이 여러개로 많이 생성될 뿐이었다.

NavigationLink와 View

NavigationLink

다음과 같이 NavigationLink를 사용하면 destination에 지정된 뷰가 미리 생성된다.

사실 back 버튼을 눌러서 뒤로 가면 기존 뷰는 사라지고 다시 생성되는 것이다. 기존 뷰가 사라지고 새로운 뷰가 생성되는 것이므로 해당 뷰의 수는 1개로 동일하다.

이렇게 미리 생성되는 것 때문에 많이 생성된 걸까 싶어서 미리 생성되는 것을 막기로 했다. 사실 아닌것 같았는데 미리 생성되는 것 자체가 싫어서 그냥 막기로 했다.

다음의 스트럭트 객체를 생성하면 된다. Viewparameter로 받기 위해서는 Generic 형태로 만들어야한다.
LazyView 만들기

struct LazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}
NavigationLink(
    destination: LazyView(ErrorView()),
    label: {
        Text("hi")
    }
).buttonStyle(PlainButtonStyle())

사용할 때에는 LazyView(SampleView()) 이런 방식으로 기존 뷰를 LazyView로 감싸서 사용하면 된다.
이렇게 했더니 destinationView가 미리 생성되는 것은 막을 수 있었다.

NavigationLinkWithLazyView

그래도 여전히 하나만 생성되야할 ImageLoader 객체가 여러개 생성되었다.

이상하게 다른 네트워크 통신하는 객체는 여러개 생성이 안되는데 이미지만 중복해서 생성되었다.

NavigationLink를 연결시킬 label: {} 부분에 들어갈contents를 변경하면서 여러 상황에서 비교해보았다.

URLImage

URLImage: 1개, ImageLoader: 5개, 네트워킹: 5번

struct ContentView: View {var body: some View {
        NavigationView {
            NavigationLink(
                destination: LazyView(ErrorView()),
                label: {
                    URLImage(urlString: "https://music-phinf.pstatic.net/20200323_53/1584950278400d35jP_PNG/VIBE__%B0%F8%C5%EB_%B9%E6%BE%C8%BF%A1%BC%AD.png")
                }
            ).buttonStyle(PlainButtonStyle())
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

NavigationLink_Content 001

ImageLoader

URLImage: 1개, ImageLoader: 1개, 네트워킹: 5번

struct ContentView: View {
    @StateObject private var imageLoader = URLImageLoader()

    var body: some View {
        NavigationView {
            NavigationLink(
                destination: LazyView(ErrorView()),
                label: {
                    if let image = imageLoader.image {
                        Image(uiImage: image)
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .onAppear {
                                imageLoader.fetch(urlString: "https://music-phinf.pstatic.net/20200323_53/1584950278400d35jP_PNG/VIBE__%B0%F8%C5%EB_%B9%E6%BE%C8%BF%A1%BC%AD.png")
                            }
                    }
                }
            ).buttonStyle(PlainButtonStyle())
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

NavigationLink_Content 002

LazyView

URLImage: 5개, ImageLoader: 5개, 네트워킹: 5번

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(
                destination: LazyView(ErrorView()),
                label: {
                    LazyView(URLImage(urlString: "https://music-phinf.pstatic.net/20200323_53/1584950278400d35jP_PNG/VIBE__%B0%F8%C5%EB_%B9%E6%BE%C8%BF%A1%BC%AD.png"))
                }
            ).buttonStyle(PlainButtonStyle())
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

NavigationLink_Content 003

 

어림도없지

아 진짜 별짓을 다해도 최종적으로 네트워크 객체 호출하는 수는 5번이었다. 하나의 이미지를 생성하려고 하면 저렇게 한 번에 다섯번이 호출되었다.

그러다가 NavigationLink 에서 뺐는데 이미지 API 호출이 원하는 대로 하나만 생겼다.👀

하... NavigationLink가 문제였구나🤦‍♀️

검색하니깐 바로 나왔다.

Navigation Link Issue with SwiftUI

여태 이미지나 네트워크 객체가 문제인줄 알고 그것만 뜯어보고 있었는데...
여튼 NavigationLinkZStack으로 분리하니 문제가 해결되었다. (사실 아님)

ZStack

URLImage: 1개, ImageLoader: 1개, 네트워킹: 1번

단! NavigationLink depth가 한 번일 경우에만!
NavigationLink depth 더 깊어지면 두 배 정도 생성된다.ㅎ

struct ContentView: View {
    var body: some View {
        NavigationView {
            ZStack {
                URLImage(urlString: "https://music-phinf.pstatic.net/20200323_53/1584950278400d35jP_PNG/VIBE__%B0%F8%C5%EB_%B9%E6%BE%C8%BF%A1%BC%AD.png")
                NavigationLink(
                    destination: LazyView(ErrorView()),
                    label: {
                        Rectangle().hidden()
                    }
                ).buttonStyle(PlainButtonStyle())
            }
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

MemorySafeNavigationLink

NavigationLink가 여기저기 얽혀있는 상태라 모든 NavigationLink 변경이 필요했다. 따라서 다음과 같이 객체로 분리하였다.

struct MemorySafeNavigationLink<Content: View>: View {
    let contentView: Content
    let destination: AnyView

    var body: some View {
        ZStack {
            contentView
            NavigationLink(
                destination: LazyView(destination),
                label: {
                    Rectangle().hidden()
                }
            )
        }
    }  
}

사용은 이런식으로 하면 된다.

let favoritesCategory = Category(playlists: viewModel.favorites,
                                 type: .favorites,
                                 mode: .half)
MemorySafeNavigationLink(
    contentView: CategoryView(category: favoritesCategory),
    destination: router.getDestination(to: .favorites)
)

해결된 줄 알았으나... 그냥 수가 준거였다. 지난 번엔 4~5배였으면 지금은 2배정도 더 생성된다.
그리고 LazyGrid에서도 문제가 있어서 하...

조금 더 망설이세요

참조