SDWebImageSwiftUI
SDWebImageSwiftUI copied to clipboard
Memory issue when caching too many GIFs in List / LazyVStack
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()
}
}
}
}
}
Doesn't have to be GIF, normal images can reproduce it too.
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).
Or you can try to use the AnimatedImage
to see if this behavior matches the same.
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.
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
That's weird, I used a real device.
@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.
OK, I'll try it later.
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.
@arakitatsuzou It's OK to upload to any Cloud File Server like Mega, or better Google Drive.
Try some magic to config the cache used for animation player.
-
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). -
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. :)
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
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.
-
There are also 142 individual GIF frame inside memory. Each of them use
2.1MB
(Means your frame pixel is like 725x725 ?), so this will consume at least 298MB . -
There are also 52 individual GIF frame consume
7.9MB
per each. Is this your own image such as placeholder or somethinig ? It does not loaded by SDWebImage (because we useinitWithCGImage:
API), this at least 411MB. -
There are also 97 individual GIF frame consume
3.27MB
per each. This at least 317.19MB. -
There are also 50 individual GIF frame consume
2.23MB
per each. This at least 111.5MB.
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)
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 ?)
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.
onevcat said the LazyVStack is buggy. So I'll bring a List reproducible demo next time.
Thanks for your reply :-)
@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.
OK, I'll try it.
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)```
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 ?
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
}
@dreampiggy any updates on this :) ?
@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.
@YuantongL Thanks! ~I'll try it soon.~
@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 :)
@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
The Issue still exists 😕