SDWebImage / SDWebImageSwiftUI

SwiftUI Image loading and Animation framework powered by SDWebImage
https://sdwebimage.github.io/SDWebImageSwiftUI
MIT License
2.16k stars 223 forks source link

Memory issue when caching too many GIFs in List / LazyVStack #172

Open tatsuz0u opened 3 years ago

tatsuz0u commented 3 years ago

Clearing memory caches manually doesn't help. Reproduced in 2.0.0 & 2.0.1.

I used 70 gifs (5-7 MB each) to reproduce with my 4GB memory iPhone.

import SwiftUI
import SDWebImageSwiftUI

struct ContentView: View {
    let gifs = // bunch of gifs here

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(gifs, id: \.hashValue) { gif in
                    WebImage(url: URL(string: gif))
                        .resizable()
                        .indicator(.progress)
                        .scaledToFit()
                }
            }
        }
    }
}
tatsuz0u commented 3 years ago

Doesn't have to be GIF, normal images can reproduce it too.

dreampiggy commented 3 years ago

Use the .purgeable modifier. By default the individual frames image cache is kept in memory (Inside the WebImage's observed ImagePlayer instance level, so not effected by global SDImageCache). even if the image is not visible (animation is stop).

dreampiggy commented 3 years ago

Or you can try to use the AnimatedImage to see if this behavior matches the same.

tatsuz0u commented 3 years ago

Thanks, I'll try that modifier :-)

But why the caches weren't cleared even if I manully call the clear method? I suppose the caches should be cleared after SDWebImage received the memory warning.

dreampiggy commented 3 years ago

It should. If you use simulator, iPhone simulator use your Mac RAM. Which may not trigger the warning notification.

See source code, which explain what we did: https://github.com/SDWebImage/SDWebImage/blob/master/SDWebImage/Core/SDAnimatedImagePlayer.m#L71-L86

tatsuz0u commented 3 years ago

That's weird, I used a real device.

dreampiggy commented 3 years ago

@arakitatsuzou Can you run Instruments Profile with Allocation or Leak to see the result ? Or upload here (Use Instruments's Save button)

You can check the Head Allocation to find, whether there are more than 70 SDAnimatedImagePlayer instances inside memory. For normal, each WebImage loading animation, will finally associated one SDAnimatedImagePlayer instance. If there are more than one, may be something issues.

I previouslly fix one issue using the Insturments there via #163. But you says you use v2.0.1, sounds strange.

tatsuz0u commented 3 years ago

OK, I'll try it later.

tatsuz0u commented 3 years ago

I finished the profile, no leaks and only one SDAnimatedImagePlayer. It crashed due to memory issue anyway.

I can upload the trace file to Mega, or full allocation summary screenshots instead?

Update: The .purgeable modifier doesn't help.

dreampiggy commented 3 years ago

@arakitatsuzou It's OK to upload to any Cloud File Server like Mega, or better Google Drive.

dreampiggy commented 3 years ago

Try some magic to config the cache used for animation player.

  1. The .maxBufferSize() modifier. Which controls each WebImage's max buffer. You can limit to a really small size (such as 1MB per WebImage), so that player will not allocate larget than buffer. But remember, this will cause CPU heavy work because we can not cache the each frame buffer and have to re-decode each time it should render on screen. (trade time for space).

  2. Don't use .avoidDecodeImage in SDWebImageOptions for animation. Which will re-draw with CGContext and transfer the RAM usage to the Core Animation render server (which is another XPC process, not in your App process). This magic will make iOS system not so aggressive to kill your App. :)

tatsuz0u commented 3 years ago

https://drive.google.com/file/d/1lPFrRLghcn5B9zkGFMLLqa27EcBYkjYD/view?usp=sharing

I tried setting .maxBufferSize() to 1024 * 1024 * 500 or 1024 * 1024 * 1 just now but it didn't work.

Check this, please :-) https://github.com/onevcat/Kingfisher/discussions/1638

dreampiggy commented 3 years ago

The trace seems does not contains the debugging symbol to show your business code...So I have to guess your use case.

There are 45 SDAnimatedImagePlayer in the memory and does never release. Is that means, your SwiftUI WebImage showing 45 WebImage at the same time ? Or your SwiftUI code using one of NagivationLink, TabView or something, which will keep the View visible or retain it. So that it does not release the RAM.

And even, there are some large NSData inside memory by using dataWithContentsOfFile:. Which consume 50MB.

So, totally: 1200MB. Which may match your current memory pressure. (1.77GB and terminated)

image


I think this is because of your own code in App, which does not manage the large amount of images. For example, you can use the thumbnailPixelSize to limit the frame size of GIF frame, or destroy some non-visible images. (Are you really want to show 45 WebImage view at the same time ?)

dreampiggy commented 3 years ago

Seems I need more useful information, like a workable demo, or at least some UI strcture for analyze. Or you can analyze by yourself with the description I talked.

WebImage is not magic. If you show large amount of animation at the same time, you'd better limit the RAM usage for each one, or you can pause some of images and setup puragable to free the GIF frame buffer which is not animating.

tatsuz0u commented 3 years ago

onevcat said the LazyVStack is buggy. So I'll bring a List reproducible demo next time.

Thanks for your reply :-)

dreampiggy commented 3 years ago

@arakitatsuzou Can AnimatedImage, which using UIViewRepresentable solve this ? Try:

List {
    ForEach(imageURLs, id: \.self) { url in
        AnimatedImage(url: url, context: [.imageThumbnailPixelSize: CGSize(width: 200, height: 200)])
            .pausable(false)
            .purgeable(true)
    }
}

This code means, when the AnimatedImage is not visible, the animating will stop and release all frame buffer. Which may suitable for your cases.

tatsuz0u commented 3 years ago

OK, I'll try it.

EthanLipnik commented 3 years ago

Having the same issue with non-animating images.


WebImage(url: focusedTweet.user?.picture, options: [.lowPriority, .scaleDownLargeImages])
                    .purgeable(true)
                    .resizable()
                    .id(focusedTweet.user?.picture)
                    .aspectRatio(1/1, contentMode: .fit)
                    .frame(width: 60)
                    .contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
                    .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
                    .contextMenu {
                        Button(action: {

                        }, label: {
                            Label("Share", systemImage: "square.and.arrow.up")
                        })
                    }
                    .shadow(color: Color("ShadowColor"), radius: 5, x: 0, y: 4)```
dreampiggy commented 3 years ago

From the testing result, the LazyVStack will not release the WebImage or Image 's memory buffer even not visible. This not works as Apple's documentation, seems a bug. See Kingfisher's same mentioned above.

Try using List ? Which is guranteed to use the native List View with UITableView, which will release the memory buffer always.

Or can anyone found good solution to solve this ?

@EthanLipnik Can you try to change the source code of SDWebImageSwiftUI here:

https://github.com/SDWebImage/SDWebImageSwiftUI/blob/master/SDWebImageSwiftUI/Classes/WebImage.swift#L27

Change all the @ObservedObject into @StateObject, which requires iOS 14+, then have a try again to see the compared result ?

tatsuz0u commented 3 years ago

Hey, I brought you a reproducible demo~

import SwiftUI
import SDWebImageSwiftUI

struct ContentView: View {
    var contents: [Content] {
        (1...102).map {
            Content(
                tag: $0,
                url: "https://github.com/tatsuz0u/Imageset/blob/main/GIFs/\($0).gif?raw=true"
            )
        }
    }

    var body: some View {
        List(contents) { content in
            ImageContainer(content: content)
        }
    }
}

// MARK: ImageContainer
private struct ImageContainer: View {
    @State var percentage: Float = 0

    var content: Content
    func placeholder(_ pageNum: Int) -> some View {
        GeometryReader { proxy in
            ZStack {
                Rectangle()
                    .fill(Color(.systemGray5))

                VStack {
                    Text("\(pageNum)")
                        .fontWeight(.bold)
                        .font(.largeTitle)
                        .foregroundColor(.gray)
                        .padding(.bottom, 15)
                    ProgressView(value: percentage, total: 1)
                        .progressViewStyle(LinearProgressViewStyle())
                        .frame(width: proxy.size.width * 0.5)
                }
            }
        }
    }

    var body: some View {
        WebImage(url: URL(string: content.url))
            .placeholder {
                placeholder(content.tag)
            }
            .onProgress(perform: onWebImageProgress)
            .resizable()
            .scaledToFit()
    }

    func onWebImageProgress(_ received: Int, _ total: Int) {
        percentage = Float(received) / Float(total)
    }
}

struct Content: Identifiable {
    var id: Int { tag }

    let tag: Int
    let url: String
}
nastasiupta commented 3 years ago

@dreampiggy any updates on this :) ?

YuantongL commented 3 years ago

@tatsuz0u Thank you for the example code, I'm able to reproduce with LazyVStack. A little bit hacky but can we do something similar to this:

public struct LazyReleaseableWebImage<T: View>: View {

    @State
    private var shouldShowImage: Bool = false

    private let content: () -> WebImage
    private let placeholder: () -> T

    public init(@ViewBuilder content: @escaping () -> WebImage,
                             @ViewBuilder placeholder: @escaping () -> T) {
        self.content = content
        self.placeholder = placeholder
    }

    public var body: some View {
        ZStack {
            if shouldShowImage {
                content()
            } else {
                placeholder()
            }
        }
        .onAppear {
            shouldShowImage = true
        }
        .onDisappear {
            shouldShowImage = false
        }
    }
}

then use

ScrollView {
    LazyVStack {
        ForEach(contents) { content in
            LazyReleaseableWebImage {
                WebImage(url: URL(string: content.url))
                    .placeholder {
                        placeholder(content.tag)
                    }
                    .onProgress(perform: onWebImageProgress)
                    .resizable()
            } placeholder: {
                placeholder(content.tag)
            }
            .scaledToFit()
        }
    }
}

seems does not cause the memory issue.

tatsuz0u commented 3 years ago

@YuantongL Thanks! ~I'll try it soon.~

ygee07 commented 3 years ago

@tatsuz0u Thank you for the example code, I'm able to reproduce with LazyVStack. A little bit hacky but can we do something similar to this:

public struct LazyReleaseableWebImage<T: View>: View {

    @State
    private var shouldShowImage: Bool = false

    private let content: () -> WebImage
    private let placeholder: () -> T

    public init(@ViewBuilder content: @escaping () -> WebImage,
                             @ViewBuilder placeholder: @escaping () -> T) {
        self.content = content
        self.placeholder = placeholder
    }

    public var body: some View {
        ZStack {
            if shouldShowImage {
                content()
            } else {
                placeholder()
            }
        }
        .onAppear {
            shouldShowImage = true
        }
        .onDisappear {
            shouldShowImage = false
        }
    }
}

then use

ScrollView {
    LazyVStack {
        ForEach(contents) { content in
            LazyReleaseableWebImage {
                WebImage(url: URL(string: content.url))
                    .placeholder {
                        placeholder(content.tag)
                    }
                    .onProgress(perform: onWebImageProgress)
                    .resizable()
            } placeholder: {
                placeholder(content.tag)
            }
            .scaledToFit()
        }
    }
}

seems does not cause the memory issue.

Also having issue with WebImage inside LazyVStack. Seems to be related with https://github.com/SDWebImage/SDWebImageSwiftUI/issues/121 This worked for me though, thanks @YuantongL :)

GorCat commented 1 year ago

@tatsuz0u感谢您提供示例代码,我可以使用 LazyVStack 进行重现。 有点 hacky 但我们可以做类似的事情吗:

public struct LazyReleaseableWebImage<T: View>: View {

    @State
    private var shouldShowImage: Bool = false

    private let content: () -> WebImage
    private let placeholder: () -> T

    public init(@ViewBuilder content: @escaping () -> WebImage,
                             @ViewBuilder placeholder: @escaping () -> T) {
        self.content = content
        self.placeholder = placeholder
    }

    public var body: some View {
        ZStack {
            if shouldShowImage {
                content()
            } else {
                placeholder()
            }
        }
        .onAppear {
            shouldShowImage = true
        }
        .onDisappear {
            shouldShowImage = false
        }
    }
}

然后使用

ScrollView {
    LazyVStack {
        ForEach(contents) { content in
            LazyReleaseableWebImage {
                WebImage(url: URL(string: content.url))
                    .placeholder {
                        placeholder(content.tag)
                    }
                    .onProgress(perform: onWebImageProgress)
                    .resizable()
            } placeholder: {
                placeholder(content.tag)
            }
            .scaledToFit()
        }
    }
}

似乎不会导致内存问题。

@tatsuz0u Thank you for the example code, I'm able to reproduce with LazyVStack. A little bit hacky but can we do something similar to this:

public struct LazyReleaseableWebImage<T: View>: View {

    @State
    private var shouldShowImage: Bool = false

    private let content: () -> WebImage
    private let placeholder: () -> T

    public init(@ViewBuilder content: @escaping () -> WebImage,
                             @ViewBuilder placeholder: @escaping () -> T) {
        self.content = content
        self.placeholder = placeholder
    }

    public var body: some View {
        ZStack {
            if shouldShowImage {
                content()
            } else {
                placeholder()
            }
        }
        .onAppear {
            shouldShowImage = true
        }
        .onDisappear {
            shouldShowImage = false
        }
    }
}

then use

ScrollView {
    LazyVStack {
        ForEach(contents) { content in
            LazyReleaseableWebImage {
                WebImage(url: URL(string: content.url))
                    .placeholder {
                        placeholder(content.tag)
                    }
                    .onProgress(perform: onWebImageProgress)
                    .resizable()
            } placeholder: {
                placeholder(content.tag)
            }
            .scaledToFit()
        }
    }
}

seems does not cause the memory issue.

It doesn't seem to work now

kasem-sm commented 1 year ago

The Issue still exists 😕